Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions registry/coder-labs/modules/codex/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.2.1"
version = "5.3.0"
agent_id = coder_agent.main.id
openai_api_key = var.openai_api_key
}
Expand All @@ -33,7 +33,7 @@ locals {

module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "5.2.1"
version = "5.3.0"
agent_id = coder_agent.main.id
workdir = local.codex_workdir
openai_api_key = var.openai_api_key
Expand Down Expand Up @@ -64,7 +64,7 @@ resource "coder_app" "codex" {
```tf
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "5.2.1"
version = "5.3.0"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
enable_ai_gateway = true
Expand All @@ -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.2.1"
version = "5.3.0"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
openai_api_key = var.openai_api_key
Expand All @@ -107,17 +107,34 @@ 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.

```tf
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "5.2.1"
version = "5.3.0"
agent_id = coder_agent.main.id
openai_api_key = var.openai_api_key
}
Expand Down
88 changes: 88 additions & 0 deletions registry/coder-labs/modules/codex/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,94 @@ describe("codex", async () => {
expect(installLog).toContain("Installed Codex CLI");
});

test("mcp-config-remote-path", async () => {
Comment thread
35C4n0r marked this conversation as resolved.
const remoteToml = [
"[mcp_servers.remote-fetched]",
'command = "remote-mcp-cmd"',
'args = ["--from-url"]',
'type = "stdio"',
].join("\n");
const { id, coderEnvVars, scripts } = await setup({
moduleVariables: {
mcp_config_remote_path: JSON.stringify([
"http://localhost:19999/mcp.toml",
"file:///tmp/remote-mcp.toml",
]),
},
});
// 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("mcp-config-remote-path-rejects-managed-markers", async () => {
const poisonedToml = [
"# >>> coder-managed: codex module >>>",
"[mcp_servers.evil]",
'command = "evil-cmd"',
'type = "stdio"',
].join("\n");
const { id, coderEnvVars, scripts } = await setup({
moduleVariables: {
mcp_config_remote_path: JSON.stringify([
"file:///tmp/poisoned-mcp.toml",
]),
},
});
await execContainer(id, [
"bash",
"-c",
`cat > /tmp/poisoned-mcp.toml <<'EOF'\n${poisonedToml}\nEOF`,
]);

await runScripts(id, scripts, coderEnvVars);

const installLog = await readFileContainer(
id,
"/home/coder/.coder-modules/coder-labs/codex/logs/install.log",
);
expect(installLog).toContain("contains managed-block markers, skipping");

const configToml = await readFileContainer(
id,
"/home/coder/.codex/config.toml",
);
expect(configToml).not.toContain("[mcp_servers.evil]");
expect(configToml).not.toContain('command = "evil-cmd"');
});

test("base-config-plus-mcp-combined", async () => {
const baseConfig = [
'sandbox_mode = "danger-full-access"',
Expand Down
7 changes: 7 additions & 0 deletions registry/coder-labs/modules/codex/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ variable "mcp" {
default = ""
}

variable "mcp_config_remote_path" {
Comment thread
35C4n0r marked this conversation as resolved.
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"
Expand Down Expand Up @@ -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
Expand Down
31 changes: 31 additions & 0 deletions registry/coder-labs/modules/codex/main.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
22 changes: 22 additions & 0 deletions registry/coder-labs/modules/codex/scripts/install.sh.tftpl
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ ARG_CODEX_VERSION='${ARG_CODEX_VERSION}'
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}'
Expand All @@ -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() {
Expand Down Expand Up @@ -166,6 +168,26 @@ function populate_config_toml() {
printf '%s\n' "$${ARG_MCP}" >> "$${managed}"
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 fetch remote MCP configurations but was not found.\n" >&2
return 1
fi
echo "$${ARG_MCP_CONFIG_REMOTE_PATH}" | jq -r '.[]' | while IFS= read -r url; do
echo "Fetching MCP configuration from: $${url}"
mcp_toml=$(curl -fsSL --connect-timeout 10 --max-time 30 "$${url}") || {
echo "Warning: Failed to fetch MCP configuration from '$${url}', continuing..."
continue
}
if printf '%s' "$${mcp_toml}" | grep -qF -e "$${MANAGED_START}" -e "$${MANAGED_END}"; then
echo "Warning: Remote MCP configuration from '$${url}' contains managed-block markers, skipping..."
continue
fi
printf "Appending MCP servers from %s\n" "$${url}"
printf '\n%s\n' "$${mcp_toml}" >> "$${managed}"
done
fi

if [ "$${ARG_ENABLE_AI_GATEWAY}" = "true" ] && [ -n "$${ARG_AIBRIDGE_CONFIG}" ]; then
if ! grep -q '\[model_providers\.aigateway\]' "$${managed}" 2>/dev/null; then
printf "Adding AI Gateway configuration\n"
Expand Down
Loading