diff --git a/registry/coder-labs/modules/codex/README.md b/registry/coder-labs/modules/codex/README.md index 08701fb1d..b43c6dd2d 100644 --- a/registry/coder-labs/modules/codex/README.md +++ b/registry/coder-labs/modules/codex/README.md @@ -13,7 +13,7 @@ Install and configure the [Codex CLI](https://github.com/openai/codex) in your w ```tf module "codex" { source = "registry.coder.com/coder-labs/codex/coder" - version = "5.0.0" + version = "5.1.0" agent_id = coder_agent.main.id openai_api_key = var.openai_api_key } @@ -33,7 +33,7 @@ locals { module "codex" { source = "registry.coder.com/coder-labs/codex/coder" - version = "5.0.0" + version = "5.1.0" agent_id = coder_agent.main.id workdir = local.codex_workdir openai_api_key = var.openai_api_key @@ -64,7 +64,7 @@ resource "coder_app" "codex" { ```tf module "codex" { source = "registry.coder.com/coder-labs/codex/coder" - version = "5.0.0" + version = "5.1.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" enable_ai_gateway = true @@ -88,7 +88,7 @@ When `enable_ai_gateway = true`, the module configures Codex to use the `aigatew ```tf module "codex" { source = "registry.coder.com/coder-labs/codex/coder" - version = "5.0.0" + version = "5.1.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" openai_api_key = var.openai_api_key @@ -107,9 +107,26 @@ module "codex" { args = ["-y", "@modelcontextprotocol/server-github"] type = "stdio" EOT + + mcp_config_remote_path = [ + "https://example.com/team-mcp-servers.toml", + "https://raw.githubusercontent.com/your-org/your-repo/main/.codex/mcp.toml", + ] } ``` +> [!NOTE] +> Servers configured through `mcp` or `mcp_config_remote_path` are appended to `~/.codex/config.toml`, so they apply to every Codex session in the workspace. Each remote URL should return a body in Codex's native TOML format, e.g.: +> +> ```toml +> [mcp_servers.my-tool] +> command = "my-tool-server" +> args = ["--port", "8080"] +> type = "stdio" +> ``` +> +> Fetch failures (network errors or non-2xx responses) log a warning and the install continues with the remaining URLs. Bodies are appended verbatim without further validation, so make sure the URL returns valid Codex TOML. + ### Serialize a downstream `coder_script` after the install pipeline The module exposes the `scripts` output: an ordered list of `coder exp sync` names for the scripts this module creates (pre_install, install, post_install). Scripts that were not configured are absent. @@ -117,7 +134,7 @@ The module exposes the `scripts` output: an ordered list of `coder exp sync` nam ```tf module "codex" { source = "registry.coder.com/coder-labs/codex/coder" - version = "5.0.0" + version = "5.1.0" agent_id = coder_agent.main.id openai_api_key = var.openai_api_key } diff --git a/registry/coder-labs/modules/codex/main.test.ts b/registry/coder-labs/modules/codex/main.test.ts index f61807723..72f9b88b7 100644 --- a/registry/coder-labs/modules/codex/main.test.ts +++ b/registry/coder-labs/modules/codex/main.test.ts @@ -425,6 +425,86 @@ describe("codex", async () => { expect(installLog).toContain("Installed Codex CLI"); }); + test("mcp-config-remote-path", async () => { + const remoteToml = [ + "[mcp_servers.remote-fetched]", + 'command = "remote-mcp-cmd"', + 'args = ["--from-url"]', + 'type = "stdio"', + ].join("\n"); + const projectDir = "/home/coder/project"; + const moduleDir = path.resolve(import.meta.dir); + const state = await runTerraformApply(moduleDir, { + agent_id: "foo", + workdir: projectDir, + install_codex: "false", + mcp_config_remote_path: JSON.stringify([ + "http://localhost:19999/mcp.toml", + "file:///tmp/remote-mcp.toml", + ]), + }); + const scripts = collectScripts(state); + const coderEnvVars = extractCoderEnvVars(state); + + const id = await runContainer("codercom/enterprise-node:latest"); + registerCleanup(async () => { + if (process.env["DEBUG"] === "true" || process.env["DEBUG"] === "1") { + console.log(`Not removing container ${id} in debug mode`); + return; + } + await removeContainer(id); + }); + + await execContainer(id, ["bash", "-c", `mkdir -p '${projectDir}'`]); + await writeExecutable({ + containerId: id, + filePath: "/usr/bin/coder", + content: "#!/bin/bash\nexit 0\n", + }); + await writeExecutable({ + containerId: id, + filePath: "/usr/bin/codex", + content: await Bun.file( + path.join(moduleDir, "testdata", "codex-mock.sh"), + ).text(), + }); + // Drop the remote TOML payload at a path the install script will fetch + // via file://. Keeps the test self-contained (no external network). + await execContainer(id, [ + "bash", + "-c", + `cat > /tmp/remote-mcp.toml <<'EOF'\n${remoteToml}\nEOF`, + ]); + + await runScripts(id, scripts, coderEnvVars); + + const installLog = await readFileContainer( + id, + "/home/coder/.coder-modules/coder-labs/codex/logs/install.log", + ); + // Both URLs were attempted. + expect(installLog).toContain("http://localhost:19999/mcp.toml"); + expect(installLog).toContain("file:///tmp/remote-mcp.toml"); + // First URL fails gracefully. + expect(installLog).toContain( + "Warning: Failed to fetch MCP configuration from 'http://localhost:19999/mcp.toml'", + ); + // Second URL succeeds. + expect(installLog).not.toContain( + "Warning: Failed to fetch MCP configuration from 'file:///tmp/remote-mcp.toml'", + ); + expect(installLog).toContain( + "Appending MCP servers from file:///tmp/remote-mcp.toml", + ); + + const configToml = await readFileContainer( + id, + "/home/coder/.codex/config.toml", + ); + expect(configToml).toContain("[mcp_servers.remote-fetched]"); + expect(configToml).toContain('command = "remote-mcp-cmd"'); + }); + test("custom-config-drops-reasoning-effort", async () => { const baseConfig = [ 'sandbox_mode = "danger-full-access"', diff --git a/registry/coder-labs/modules/codex/main.tf b/registry/coder-labs/modules/codex/main.tf index c23129bc6..c93f48abd 100644 --- a/registry/coder-labs/modules/codex/main.tf +++ b/registry/coder-labs/modules/codex/main.tf @@ -88,6 +88,12 @@ variable "mcp" { default = "" } +variable "mcp_config_remote_path" { + type = list(string) + description = "List of URLs that return MCP server configurations in TOML format (matching Codex's native config format). Fetched at install time and appended to config.toml." + default = [] +} + variable "model_reasoning_effort" { type = string description = "The reasoning effort for the model. One of: none, minimal, low, medium, high, xhigh. See https://platform.openai.com/docs/guides/latest-model#lower-reasoning-effort" @@ -141,6 +147,7 @@ locals { ARG_WORKDIR = local.workdir != "" ? base64encode(local.workdir) : "" ARG_BASE_CONFIG_TOML = var.base_config_toml != "" ? base64encode(var.base_config_toml) : "" ARG_MCP = var.mcp != "" ? base64encode(var.mcp) : "" + ARG_MCP_CONFIG_REMOTE_PATH = base64encode(jsonencode(var.mcp_config_remote_path)) ARG_ENABLE_AI_GATEWAY = tostring(var.enable_ai_gateway) ARG_AIBRIDGE_CONFIG = var.enable_ai_gateway ? base64encode(local.aibridge_config) : "" ARG_MODEL_REASONING_EFFORT = var.model_reasoning_effort diff --git a/registry/coder-labs/modules/codex/main.tftest.hcl b/registry/coder-labs/modules/codex/main.tftest.hcl index 3bcd681ac..c4c7403f3 100644 --- a/registry/coder-labs/modules/codex/main.tftest.hcl +++ b/registry/coder-labs/modules/codex/main.tftest.hcl @@ -183,3 +183,34 @@ run "test_workdir_optional" { error_message = "scripts output should have install script even without workdir" } } + +run "test_mcp_config_remote_path" { + command = plan + + variables { + agent_id = "test-agent" + workdir = "/home/coder" + mcp_config_remote_path = [ + "https://example.com/mcp-one.toml", + "https://example.com/mcp-two.toml", + ] + } + + assert { + condition = strcontains(local.install_script, base64encode(jsonencode(var.mcp_config_remote_path))) + error_message = "install script should embed the base64-encoded mcp_config_remote_path JSON" + } +} + +run "test_mcp_config_remote_path_default" { + command = plan + + variables { + agent_id = "test-agent" + } + + assert { + condition = length(var.mcp_config_remote_path) == 0 + error_message = "mcp_config_remote_path should default to an empty list" + } +} diff --git a/registry/coder-labs/modules/codex/scripts/install.sh.tftpl b/registry/coder-labs/modules/codex/scripts/install.sh.tftpl index 584c978b3..f7e63a295 100644 --- a/registry/coder-labs/modules/codex/scripts/install.sh.tftpl +++ b/registry/coder-labs/modules/codex/scripts/install.sh.tftpl @@ -13,6 +13,7 @@ ARG_CODEX_VERSION=$(echo -n '${ARG_CODEX_VERSION}' | base64 -d) ARG_WORKDIR=$(echo -n '${ARG_WORKDIR}' | base64 -d) ARG_BASE_CONFIG_TOML=$(echo -n '${ARG_BASE_CONFIG_TOML}' | base64 -d) ARG_MCP=$(echo -n '${ARG_MCP}' | base64 -d) +ARG_MCP_CONFIG_REMOTE_PATH=$(echo -n '${ARG_MCP_CONFIG_REMOTE_PATH}' | base64 -d) ARG_ENABLE_AI_GATEWAY='${ARG_ENABLE_AI_GATEWAY}' ARG_AIBRIDGE_CONFIG=$(echo -n '${ARG_AIBRIDGE_CONFIG}' | base64 -d) ARG_MODEL_REASONING_EFFORT='${ARG_MODEL_REASONING_EFFORT}' @@ -24,6 +25,7 @@ printf "workdir: %s\n" "$${ARG_WORKDIR}" printf "enable_ai_gateway: %s\n" "$${ARG_ENABLE_AI_GATEWAY}" printf "install_codex: %s\n" "$${ARG_INSTALL}" printf "model_reasoning_effort: %s\n" "$${ARG_MODEL_REASONING_EFFORT}" +printf "mcp_config_remote_path: %s\n" "$${ARG_MCP_CONFIG_REMOTE_PATH}" echo "--------------------------------" function add_path_to_shell_profiles() { @@ -155,6 +157,22 @@ function populate_config_toml() { echo "$${ARG_MCP}" >> "$${config_path}" fi + if [ -n "$${ARG_MCP_CONFIG_REMOTE_PATH}" ] && [ "$${ARG_MCP_CONFIG_REMOTE_PATH}" != "[]" ]; then + if ! command -v jq > /dev/null 2>&1; then + printf "Error: 'jq' is required to process mcp_config_remote_path but was not found. Skipping remote MCP config fetch.\n" >&2 + else + for url in $(echo "$${ARG_MCP_CONFIG_REMOTE_PATH}" | jq -r '.[]'); do + echo "Fetching MCP configuration from: $${url}" + mcp_toml=$(curl -fsSL "$${url}") || { + echo "Warning: Failed to fetch MCP configuration from '$${url}', continuing..." + continue + } + printf "Appending MCP servers from %s\n" "$${url}" + printf '\n%s\n' "$${mcp_toml}" >> "$${config_path}" + done + fi + fi + if [ "$${ARG_ENABLE_AI_GATEWAY}" = "true" ] && [ -n "$${ARG_AIBRIDGE_CONFIG}" ]; then if ! grep -q '\[model_providers\.aigateway\]' "$${config_path}" 2>/dev/null; then printf "Adding AI Gateway configuration\n"