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/matifali/README.md b/registry/matifali/README.md new file mode 100644 index 000000000..ae37e181a --- /dev/null +++ b/registry/matifali/README.md @@ -0,0 +1,15 @@ +--- +display_name: "Muhammad Atif Ali" +bio: "Developer Experience and Ecosystem at Coder, focused on IDEs, agents, AI governance, and platform polish." +github: "matifali" +website: "https://matifali.dev" +status: "community" +--- + +# Muhammad Atif Ali + +Developer Experience and Ecosystem at Coder, focused on IDEs, agents, AI governance, and platform polish. + +## Modules + +- **omnigent**: Run a private Omnigent multi-agent coding server in your Coder workspace. diff --git a/registry/matifali/modules/omnigent/README.md b/registry/matifali/modules/omnigent/README.md new file mode 100644 index 000000000..94d012540 --- /dev/null +++ b/registry/matifali/modules/omnigent/README.md @@ -0,0 +1,275 @@ +--- +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/matifali/omnigent/coder" + version = "0.0.1" + agent_id = coder_agent.main.id +} +``` + +## Use in a template + +Add this module to any Coder template that has a Linux `coder_agent`. The module only needs the agent ID. It handles installing `uv`, installing Omnigent, starting the app server, and registering the workspace as an Omnigent host. + +For the best multi-agent experience, install and configure any local agent CLIs before the Omnigent host starts. Omnigent snapshots the host's available tools at startup, so Claude Code, Codex, or other harnesses should finish setup first. + +### Minimal existing-template integration + +Use this when your template already has an agent and you only want the Omnigent app. + +```tf +module "omnigent" { + source = "registry.coder.com/matifali/omnigent/coder" + version = "0.0.1" + + agent_id = coder_agent.main.id +} +``` + +### Full AI workspace integration + +Use this pattern when you want Omnigent, Claude Code, Codex, Coder AI Gateway, and a repository workdir in any existing Docker, Kubernetes, or VM template. Replace the Git URL and dependency installer for your base image as needed. The dependency script below targets Ubuntu or Debian images with `apt-get`. + +```tf +locals { + repo_ready_sync_name = "matifali-omnigent-git-clone" + + ai_tools_pre_install_commands = <<-EOT + missing_packages=() + command -v curl >/dev/null 2>&1 || missing_packages+=(curl) + command -v jq >/dev/null 2>&1 || missing_packages+=(jq) + command -v tmux >/dev/null 2>&1 || missing_packages+=(tmux) + command -v bwrap >/dev/null 2>&1 || missing_packages+=(bubblewrap) + + need_node=false + if ! command -v node >/dev/null 2>&1; then + need_node=true + elif ! node -e 'process.exit(Number(process.versions.node.split(".")[0]) >= 22 ? 0 : 1)' >/dev/null 2>&1; then + need_node=true + fi + + if [ "$${#missing_packages[@]}" -eq 0 ] && [ "$${need_node}" = false ]; then + exit 0 + fi + + if ! command -v apt-get >/dev/null 2>&1; then + echo "ERROR: missing required AI tool dependencies and apt-get is not available." >&2 + printf 'Missing packages: %s\n' "$${missing_packages[*]:-none}" >&2 + printf 'Need Node.js 22+: %s\n' "$${need_node}" >&2 + exit 1 + fi + + ( + flock 9 + sudo apt-get update + + if [ "$${need_node}" = true ]; then + if ! command -v curl >/dev/null 2>&1; then + sudo apt-get install -y curl ca-certificates + fi + curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - + sudo apt-get install -y nodejs + fi + + if [ "$${#missing_packages[@]}" -gt 0 ]; then + sudo apt-get install -y ca-certificates "$${missing_packages[@]}" + fi + ) 9>/tmp/coder-ai-tools-apt.lock + EOT + + codex_pre_install_script = <<-EOT + #!/bin/bash + set -euo pipefail + coder exp sync want matifali-codex-repo-ready ${local.repo_ready_sync_name} + coder exp sync start matifali-codex-repo-ready + coder exp sync complete matifali-codex-repo-ready + + ${local.ai_tools_pre_install_commands} + EOT + + claude_code_pre_install_script = <<-EOT + #!/bin/bash + set -euo pipefail + coder exp sync want matifali-claude-code-repo-ready ${local.repo_ready_sync_name} + coder exp sync start matifali-claude-code-repo-ready + coder exp sync complete matifali-claude-code-repo-ready + + ${local.ai_tools_pre_install_commands} + EOT +} + +module "git_clone" { + source = "registry.coder.com/coder/git-clone/coder" + version = "2.0.1" + + agent_id = coder_agent.main.id + url = "https://github.com/coder/coder" + base_dir = "/home/coder/workspace" + folder_name = "coder" + extra_args = ["--depth=1"] + + post_clone_script = <<-EOT + #!/bin/bash + set -euo pipefail + coder exp sync start ${local.repo_ready_sync_name} + coder exp sync complete ${local.repo_ready_sync_name} + EOT +} + +module "codex" { + source = "registry.coder.com/coder-labs/codex/coder" + version = "5.2.1" + + agent_id = coder_agent.main.id + workdir = module.git_clone.repo_dir + enable_ai_gateway = true + pre_install_script = local.codex_pre_install_script +} + +module "claude_code" { + source = "registry.coder.com/coder/claude-code/coder" + version = "5.2.0" + + agent_id = coder_agent.main.id + workdir = module.git_clone.repo_dir + enable_ai_gateway = true + pre_install_script = local.claude_code_pre_install_script +} + +module "omnigent" { + source = "registry.coder.com/matifali/omnigent/coder" + version = "0.0.1" + + agent_id = coder_agent.main.id + + # Wait for Claude Code and Codex setup before Omnigent snapshots host tools. + pre_install_script = <<-EOT + #!/bin/bash + set -euo pipefail + coder exp sync want matifali-omnigent-ai-tools ${join(" ", concat(module.claude_code.scripts, module.codex.scripts))} + coder exp sync start matifali-omnigent-ai-tools + coder exp sync complete matifali-omnigent-ai-tools + EOT +} +``` + +## Configuration examples + +### Custom port + +```tf +module "omnigent" { + source = "registry.coder.com/matifali/omnigent/coder" + version = "0.0.1" + agent_id = coder_agent.main.id + port = 7878 +} +``` + +### Additional trusted origins + +The module automatically trusts Coder app origins derived from `CODER_AGENT_URL` and `VSCODE_PROXY_URI` when those environment variables are available. If you expose Omnigent through another reverse proxy, add that browser origin explicitly: + +```tf +module "omnigent" { + source = "registry.coder.com/matifali/omnigent/coder" + version = "0.0.1" + agent_id = coder_agent.main.id + + allowed_origins = ["https://omnigent.example.com"] +} +``` + +### Policies, server-wide + +```tf +module "omnigent" { + source = "registry.coder.com/matifali/omnigent/coder" + version = "0.0.1" + 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/matifali/omnigent/coder" + version = "0.0.1" + 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 server config file + +```tf +module "omnigent" { + source = "registry.coder.com/matifali/omnigent/coder" + version = "0.0.1" + agent_id = coder_agent.main.id + server_config_path = "/home/coder/.omnigent/server_config.yaml" +} +``` + +## Troubleshooting + +Script logs are written to `~/.coder-modules/matifali/omnigent/logs/`. If the Omnigent app shows as unhealthy or the server fails to start, check: + +```bash +cat ~/.coder-modules/matifali/omnigent/logs/server.log +cat ~/.coder-modules/matifali/omnigent/logs/start.log +cat ~/.coder-modules/matifali/omnigent/logs/install.log +cat ~/.coder-modules/matifali/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/matifali/modules/omnigent/main.tf b/registry/matifali/modules/omnigent/main.tf new file mode 100644 index 000000000..658c03624 --- /dev/null +++ b/registry/matifali/modules/omnigent/main.tf @@ -0,0 +1,189 @@ +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 "allowed_origins" { + description = "Additional trusted browser origins for Omnigent HTTP/WebSocket CSRF checks. Use this when exposing Omnigent through a reverse proxy not covered by the automatic Coder app origin detection." + type = list(string) + default = [] + validation { + condition = alltrue([ + for origin in var.allowed_origins : trimspace(origin) == origin && can(regex("^https?://[^/?#,[:space:]]+$", origin)) + ]) + error_message = "allowed_origins entries must be origins like https://omnigent.example.com (scheme, host, optional port; no path)." + } +} + +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 = [] + validation { + condition = alltrue([ + for agent in var.agents : ( + length(trimspace(agent.name)) > 0 && + !strcontains(agent.name, "\t") && + !strcontains(agent.name, "\n") && + !strcontains(agent.name, "\r") + ) + ]) + error_message = "agents entries must have a non-empty name without tab or newline characters." + } +} + +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/matifali/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 + ARG_ALLOWED_ORIGINS_B64 = base64encode(join(",", var.allowed_origins)) + }) +} + +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/matifali/modules/omnigent/main.tftest.hcl b/registry/matifali/modules/omnigent/main.tftest.hcl new file mode 100644 index 000000000..9eb7364a9 --- /dev/null +++ b/registry/matifali/modules/omnigent/main.tftest.hcl @@ -0,0 +1,382 @@ +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_allowed_origins" { + command = plan + + variables { + agent_id = "test-agent" + allowed_origins = ["https://omnigent--workspace--owner--apps.example.com"] + } + + assert { + condition = contains(var.allowed_origins, "https://omnigent--workspace--owner--apps.example.com") + error_message = "allowed_origins should include the configured origin" + } + + assert { + condition = strcontains(local.start_script, "OMNIGENT_WS_ALLOWED_ORIGINS") + error_message = "start script should export Omnigent's trusted origin allowlist" + } + + assert { + condition = strcontains(local.start_script, base64encode("https://omnigent--workspace--owner--apps.example.com")) + error_message = "start script should include the encoded configured origin" + } + + assert { + condition = strcontains(local.start_script, "CODER_AGENT_URL") + error_message = "start script should allow the path-based Coder app origin" + } + + assert { + condition = strcontains(local.start_script, "VSCODE_PROXY_URI") + error_message = "start script should derive Coder app origins from VSCODE_PROXY_URI" + } + + assert { + condition = strcontains(local.start_script, "omnigent--") + error_message = "start script should allow the named Omnigent Coder app origin" + } +} + +run "test_invalid_allowed_origin_path" { + command = plan + + variables { + agent_id = "test-agent" + allowed_origins = ["https://omnigent.example.com/path"] + } + + expect_failures = [var.allowed_origins] +} + +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_install_script_clears_stale_agents" { + command = plan + + variables { + agent_id = "test-agent" + } + + assert { + condition = strcontains(local.install_script, "rm -f \"$${ARG_AGENTS_DIR}\"/*.yaml") + error_message = "install script should clear stale generated agent YAML files" + } +} + +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_start_script_quotes_server_flags" { + command = plan + + variables { + agent_id = "test-agent" + } + + assert { + condition = strcontains(local.start_script, "SERVER_FLAGS=(") + error_message = "start script should build server flags as a bash array" + } + + assert { + condition = strcontains(local.start_script, "omnigent server \"$${SERVER_FLAGS[@]}\"") + error_message = "start script should pass server flags without word splitting" + } +} + +run "test_start_script_connects_host_to_app_server" { + command = plan + + variables { + agent_id = "test-agent" + } + + assert { + condition = strcontains(local.start_script, "omnigent host --server") + error_message = "start script should connect the host to the Coder app server" + } + + assert { + condition = strcontains(local.start_script, "http://localhost:$${ARG_PORT}") + error_message = "start script should pass the configured Omnigent server port to the host" + } +} + +run "test_start_script_passes_ai_gateway_token_to_runners" { + command = plan + + variables { + agent_id = "test-agent" + } + + assert { + condition = strcontains(local.start_script, "append_runner_env_passthrough") + error_message = "start script should append runner env passthrough entries" + } + + assert { + condition = strcontains(local.start_script, "OPENAI_CODER_AIGATEWAY_SESSION_TOKEN") + error_message = "start script should pass the Coder AI Gateway OpenAI token to Omnigent runners" + } + + assert { + condition = strcontains(local.start_script, "OMNIGENT_RUNNER_ENV_PASSTHROUGH=\"$${OMNIGENT_RUNNER_ENV_PASSTHROUGH},$${name}\"") + error_message = "start script should preserve existing runner env passthrough values" + } + + assert { + condition = strcontains(local.start_script, "*\",$${name},\"*) ;;") + error_message = "start script should avoid duplicate runner env passthrough entries" + } +} + +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_invalid_agent_name" { + command = plan + + variables { + agent_id = "test-agent" + agents = [ + { + name = "bad\tname" + content = "name: reviewer\ninstructions: You are a reviewer." + } + ] + } + + expect_failures = [var.agents] +} + +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/matifali/modules/omnigent/scripts/install.sh.tftpl b/registry/matifali/modules/omnigent/scripts/install.sh.tftpl new file mode 100644 index 000000000..e0df64911 --- /dev/null +++ b/registry/matifali/modules/omnigent/scripts/install.sh.tftpl @@ -0,0 +1,78 @@ +#!/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}" + printf '%s\n' "$${ARG_SERVER_CONFIG}" > "$${ARG_SERVER_CONFIG_FILE}" +fi + +# Write agent YAML files. Clear stale generated files first so removed agents +# are not re-registered on the next start. +mkdir -p "$${ARG_AGENTS_DIR}" +rm -f "$${ARG_AGENTS_DIR}"/*.yaml +if [ -n "$${ARG_AGENTS}" ]; then + agent_index=0 + while IFS=$' ' read -r agent_name agent_content_b64; do + [ -z "$${agent_name}" ] && continue + agent_index=$((agent_index + 1)) + agent_file="$${ARG_AGENTS_DIR}/agent-$${agent_index}.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/matifali/modules/omnigent/scripts/start.sh.tftpl b/registry/matifali/modules/omnigent/scripts/start.sh.tftpl new file mode 100644 index 000000000..8810c36ee --- /dev/null +++ b/registry/matifali/modules/omnigent/scripts/start.sh.tftpl @@ -0,0 +1,136 @@ +#!/bin/bash +set -euo pipefail + +export PATH="$${HOME}/.local/bin:$${PATH}" + +MODULE_DIR="$${HOME}/.coder-modules/matifali/omnigent" +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}' +ARG_ALLOWED_ORIGINS=$(echo -n '${ARG_ALLOWED_ORIGINS_B64}' | base64 -d) + +mkdir -p "$${MODULE_DIR}/logs" + +append_allowed_origin() { + local origin="$${1:-}" + [ -n "$${origin}" ] || return 0 + case ",$${OMNIGENT_ALLOWED_ORIGINS}," in + *",$${origin},"*) ;; + *) + if [ -n "$${OMNIGENT_ALLOWED_ORIGINS}" ]; then + OMNIGENT_ALLOWED_ORIGINS="$${OMNIGENT_ALLOWED_ORIGINS},$${origin}" + else + OMNIGENT_ALLOWED_ORIGINS="$${origin}" + fi + ;; + esac +} + +origin_from_url() { + local url="$${1:-}" + case "$${url}" in + http://*|https://*) + printf '%s\n' "$${url}" | sed -E 's#^(https?://[^/]+).*#\1#' | tr '[:upper:]' '[:lower:]' + ;; + esac +} + +append_runner_env_passthrough() { + local name="$${1:-}" + [ -n "$${name}" ] || return 0 + [ -n "$${!name:-}" ] || return 0 + case ",$${OMNIGENT_RUNNER_ENV_PASSTHROUGH:-}," in + *",$${name},"*) ;; + *) + if [ -n "$${OMNIGENT_RUNNER_ENV_PASSTHROUGH:-}" ]; then + OMNIGENT_RUNNER_ENV_PASSTHROUGH="$${OMNIGENT_RUNNER_ENV_PASSTHROUGH},$${name}" + else + OMNIGENT_RUNNER_ENV_PASSTHROUGH="$${name}" + fi + ;; + esac + export OMNIGENT_RUNNER_ENV_PASSTHROUGH +} + +OMNIGENT_ALLOWED_ORIGINS="$${ARG_ALLOWED_ORIGINS}" +append_allowed_origin "$(origin_from_url "$${CODER_AGENT_URL:-}")" + +# Coder app requests arrive with the browser Origin set to the app proxy URL. +# Trust both the named app URL and the direct port-forward URL when Coder exposes +# VSCODE_PROXY_URI in the workspace environment. +if [ -n "$${VSCODE_PROXY_URI:-}" ]; then + port_proxy_url="$${VSCODE_PROXY_URI//\{\{port\}\}/$${ARG_PORT}}" + append_allowed_origin "$(origin_from_url "$${port_proxy_url}")" + + if [ -n "$${CODER_WORKSPACE_AGENT_NAME:-}" ]; then + named_app_url="$${VSCODE_PROXY_URI//\{\{port\}\}--$${CODER_WORKSPACE_AGENT_NAME}--/omnigent--}" + if [ "$${named_app_url}" != "$${VSCODE_PROXY_URI}" ]; then + append_allowed_origin "$(origin_from_url "$${named_app_url}")" + fi + fi +fi + +# Omnigent host runners only receive a limited environment allowlist. Include +# Coder AI Gateway's OpenAI token when the workspace exposes it so Codex can +# authenticate from sessions launched through Omnigent. +append_runner_env_passthrough "OPENAI_CODER_AIGATEWAY_SESSION_TOKEN" + +# 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+=(-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+=(--agent "$${agent_file}") + echo "Registering agent: $${agent_file}" + done + fi + + export OMNIGENT_ACCOUNTS_INIT_ADMIN_PASSWORD="$${OMNIGENT_ADMIN_PASSWORD}" + if [ -n "$${OMNIGENT_ALLOWED_ORIGINS}" ]; then + export OMNIGENT_WS_ALLOWED_ORIGINS="$${OMNIGENT_ALLOWED_ORIGINS}" + echo "Allowing Omnigent browser origins: $${OMNIGENT_WS_ALLOWED_ORIGINS}" + fi + 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 against the Coder app server. Passing an +# empty server URL makes Omnigent spawn and connect to a separate local server, +# leaving the Coder app server with an offline host record for filesystem calls. +if ! pgrep -f "[o]mnigent host.*http://localhost:$${ARG_PORT}" >/dev/null 2>&1; then + echo "Starting Omnigent host..." + nohup omnigent host --server "http://localhost:$${ARG_PORT}" >> "$${HOST_LOG}" 2>&1 & +else + echo "Omnigent host already running, skipping start." +fi