From a1a37e4393a4cea1546eda2fe2c0f7472e6861d4 Mon Sep 17 00:00:00 2001 From: Jay Kumar Date: Tue, 28 Apr 2026 13:45:33 +0000 Subject: [PATCH 01/25] refactor(registry/coder-labs/modules/codex): replace agentapi with coder-utils, remove start script Mirror the claude-code refactor from #861: - Replace module agentapi with module coder_utils - Replace install.sh with install.sh.tftpl (templatefile) - Delete start.sh entirely - Remove all AgentAPI, boundary, and start-script-only variables - Rename enable_aibridge to enable_ai_gateway - Make workdir optional (default null) - Output scripts list instead of task_app_id - Conditional env var resources with count - Update tests and README --- registry/coder-labs/modules/codex/README.md | 196 +++--- .../coder-labs/modules/codex/main.test.ts | 593 ++++++------------ registry/coder-labs/modules/codex/main.tf | 277 ++------ .../coder-labs/modules/codex/main.tftest.hcl | 201 +++--- .../modules/codex/scripts/install.sh | 228 ------- .../modules/codex/scripts/install.sh.tftpl | 152 +++++ .../coder-labs/modules/codex/scripts/start.sh | 229 ------- .../modules/codex/testdata/codex-mock.sh | 33 +- 8 files changed, 603 insertions(+), 1306 deletions(-) delete mode 100644 registry/coder-labs/modules/codex/scripts/install.sh create mode 100644 registry/coder-labs/modules/codex/scripts/install.sh.tftpl delete mode 100644 registry/coder-labs/modules/codex/scripts/start.sh diff --git a/registry/coder-labs/modules/codex/README.md b/registry/coder-labs/modules/codex/README.md index 16786d023..0ee45e186 100644 --- a/registry/coder-labs/modules/codex/README.md +++ b/registry/coder-labs/modules/codex/README.md @@ -1,148 +1,105 @@ --- display_name: Codex CLI icon: ../../../../.icons/openai.svg -description: Run Codex CLI in your workspace with AgentAPI integration +description: Install and configure the Codex CLI in your workspace. verified: true -tags: [agent, codex, ai, openai, tasks, aibridge] +tags: [agent, codex, ai, openai, ai-gateway] --- # Codex CLI -Run Codex CLI in your workspace to access OpenAI's models through the Codex interface, with custom pre/post install scripts. This module integrates with [AgentAPI](https://github.com/coder/agentapi) for Coder Tasks compatibility. +Install and configure the [Codex CLI](https://github.com/openai/codex) in your workspace. Starting Codex is left to the caller (template command, IDE launcher, or a custom `coder_script`). ```tf module "codex" { source = "registry.coder.com/coder-labs/codex/coder" - version = "4.3.1" - agent_id = coder_agent.example.id + version = "5.0.0" + agent_id = coder_agent.main.id openai_api_key = var.openai_api_key - workdir = "/home/coder/project" } ``` -## Prerequisites - -- OpenAI API key for Codex access - -## Examples - -### Run standalone - -```tf -module "codex" { - count = data.coder_workspace.me.start_count - source = "registry.coder.com/coder-labs/codex/coder" - version = "4.3.1" - agent_id = coder_agent.example.id - openai_api_key = "..." - workdir = "/home/coder/project" - report_tasks = false -} -``` - -### Usage with AI Bridge - -[AI Bridge](https://coder.com/docs/ai-coder/ai-bridge) is a Premium Coder feature that provides centralized LLM proxy management. To use AI Bridge, set `enable_aibridge = true`. Requires Coder version 2.30+ - -For tasks integration with AI Bridge, add `enable_aibridge = true` to the [Usage with Tasks](#usage-with-tasks) example below. - -#### Standalone usage with AI Bridge +> [!WARNING] +> If upgrading from v4.x.x of this module: v5 is a major refactor that drops support for [Coder Tasks](https://coder.com/docs/ai-coder/tasks) and [Boundary](https://coder.com/docs/ai-coder/agent-firewall). Keep using v4.x.x if you depend on them. -```tf -module "codex" { - source = "registry.coder.com/coder-labs/codex/coder" - version = "4.3.1" - agent_id = coder_agent.example.id - workdir = "/home/coder/project" - enable_aibridge = true -} -``` +## Prerequisites -When `enable_aibridge = true`, the module: +- OpenAI API key, or Coder AI Gateway (`enable_ai_gateway = true`, requires Coder >= 2.30.0). -- Configures Codex to use the aibridge model_provider with `base_url` pointing to `${data.coder_workspace.me.access_url}/api/v2/aibridge/openai/v1` and `env_key` pointing to the workspace owner's session token +## workdir -```toml -model_provider = "aibridge" +`workdir` is optional. When set, the module pre-creates the directory if it is missing and adds it as a trusted project in `~/.codex/config.toml`. Leave `workdir` unset if you only want the module to install the CLI and configure authentication. -[model_providers.aibridge] -name = "AI Bridge" -base_url = "https://example.coder.com/api/v2/aibridge/openai/v1" -env_key = "CODER_AIBRIDGE_SESSION_TOKEN" -wire_api = "responses" -``` - -This allows Codex to route API requests through Coder's AI Bridge instead of directly to OpenAI's API. -Template build will fail if `openai_api_key` is provided alongside `enable_aibridge = true`. - -### Usage with Tasks +## Examples -This example shows how to configure Codex with Coder tasks. +### Standalone mode with a launcher app ```tf -resource "coder_ai_task" "task" { - count = data.coder_workspace.me.start_count - app_id = module.codex.task_app_id +locals { + codex_workdir = "/home/coder/project" } -data "coder_task" "me" {} - module "codex" { source = "registry.coder.com/coder-labs/codex/coder" - version = "4.3.1" - agent_id = coder_agent.example.id - openai_api_key = "..." - ai_prompt = data.coder_task.me.prompt - workdir = "/home/coder/project" + version = "5.0.0" + agent_id = coder_agent.main.id + workdir = local.codex_workdir + openai_api_key = var.openai_api_key +} - # Optional: route through AI Bridge (Premium feature) - # enable_aibridge = true +resource "coder_app" "codex" { + agent_id = coder_agent.main.id + slug = "codex" + display_name = "Codex" + icon = "/icon/openai.svg" + open_in = "slim-window" + command = <<-EOT + #!/bin/bash + set -e + cd ${local.codex_workdir} + codex + EOT } ``` -### Usage with Agent Boundaries - -This example shows how to configure the Codex module to run the agent behind a process-level boundary that restricts its network access. +### Usage with AI Gateway -By default, when `enable_boundary = true`, the module uses `coder boundary` subcommand (provided by Coder) without requiring any installation. +[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. ```tf module "codex" { - source = "registry.coder.com/coder-labs/codex/coder" - version = "4.3.1" - agent_id = coder_agent.main.id - openai_api_key = var.openai_api_key - workdir = "/home/coder/project" - enable_boundary = true + source = "registry.coder.com/coder-labs/codex/coder" + version = "5.0.0" + agent_id = coder_agent.main.id + workdir = "/home/coder/project" + enable_ai_gateway = true } ``` -> [!NOTE] -> For developers: The module also supports installing boundary from a release version (`use_boundary_directly = true`) or compiling from source (`compile_boundary_from_source = true`). These are escape hatches for development and testing purposes. +When `enable_ai_gateway = true`, the module configures Codex to use the `aibridge` model provider in `config.toml` with the workspace owner's session token for authentication. -### Advanced Configuration +> [!CAUTION] +> `enable_ai_gateway = true` is mutually exclusive with `openai_api_key`. Setting both fails at plan time. -This example shows additional configuration options for custom models, MCP servers, and base configuration. +### Advanced Configuration ```tf module "codex" { source = "registry.coder.com/coder-labs/codex/coder" - version = "4.3.1" - agent_id = coder_agent.example.id - openai_api_key = "..." + version = "5.0.0" + agent_id = coder_agent.main.id workdir = "/home/coder/project" + openai_api_key = var.openai_api_key - codex_version = "0.1.0" # Pin to a specific version - codex_model = "gpt-4o" # Custom model + codex_version = "0.1.0" + codex_model = "gpt-4o" - # Override default configuration base_config_toml = <<-EOT sandbox_mode = "danger-full-access" approval_policy = "never" preferred_auth_method = "apikey" EOT - # Add extra MCP servers additional_mcp_servers = <<-EOT [mcp_servers.GitHub] command = "npx" @@ -152,27 +109,31 @@ module "codex" { } ``` -> [!WARNING] -> This module configures Codex with a `workspace-write` sandbox that allows AI tasks to read/write files in the specified workdir. While the sandbox provides security boundaries, Codex can still modify files within the workspace. Use this module _only_ in trusted environments and be aware of the security implications. - -## How it Works - -- **Install**: The module installs Codex CLI and sets up the environment -- **System Prompt**: If `codex_system_prompt` is set, writes the prompt to `AGENTS.md` in the `~/.codex/` directory -- **Start**: Launches Codex CLI in the specified directory, wrapped by AgentAPI -- **Configuration**: Sets `OPENAI_API_KEY` environment variable and passes `--model` flag to Codex CLI (if variables provided) -- **Session Continuity**: When `continue = true` (default), the module automatically tracks task sessions in `~/.codex-module/.codex-task-session`. On workspace restart, it resumes the existing session with full conversation history. Set `continue = false` to always start fresh sessions. - -## State Persistence - -AgentAPI can save and restore its conversation state to disk across workspace restarts. This complements `continue` (which resumes the Codex CLI session) by also preserving the AgentAPI-level context. Enabled by default, requires agentapi >= v0.12.0 (older versions skip it with a warning). +### Serialize a downstream `coder_script` after the install pipeline -To disable: +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" { - # ... other config - enable_state_persistence = false + source = "registry.coder.com/coder-labs/codex/coder" + version = "5.0.0" + agent_id = coder_agent.main.id + openai_api_key = var.openai_api_key +} + +resource "coder_script" "post_codex" { + agent_id = coder_agent.main.id + display_name = "Run after Codex install" + run_on_start = true + script = <<-EOT + #!/bin/bash + set -euo pipefail + trap 'coder exp sync complete post-codex' EXIT + coder exp sync want post-codex ${join(" ", module.codex.scripts)} + coder exp sync start post-codex + + codex --version + EOT } ``` @@ -180,7 +141,7 @@ module "codex" { ### Default Configuration -When no custom `base_config_toml` is provided, the module uses these secure defaults: +When no custom `base_config_toml` is provided, the module uses these defaults: ```toml sandbox_mode = "workspace-write" @@ -192,21 +153,20 @@ network_access = true ``` > [!NOTE] -> If no custom configuration is provided, the module uses secure defaults. The Coder MCP server is always included automatically. For containerized workspaces (Docker/Kubernetes), you may need `sandbox_mode = "danger-full-access"` to avoid permission issues. For advanced options, see [Codex config docs](https://github.com/openai/codex/blob/main/codex-rs/config.md). +> For containerized workspaces (Docker/Kubernetes), you may need `sandbox_mode = "danger-full-access"` to avoid permission issues. For advanced options, see [Codex config docs](https://github.com/openai/codex/blob/main/codex-rs/config.md). ## Troubleshooting -- Check installation and startup logs in `~/.codex-module/` -- Ensure your OpenAI API key has access to the specified model +Check the log files in `~/.coder-modules/coder-labs/codex/logs/` for detailed information. -> [!IMPORTANT] -> To use tasks with Codex CLI, ensure you have the `openai_api_key` variable set. [Tasks Template Example](https://registry.coder.com/templates/coder-labs/tasks-docker). -> The module automatically configures Codex with your API key and model preferences. -> workdir is a required variable for the module to function correctly. +```bash +cat ~/.coder-modules/coder-labs/codex/logs/install.log +cat ~/.coder-modules/coder-labs/codex/logs/pre_install.log +cat ~/.coder-modules/coder-labs/codex/logs/post_install.log +``` ## References - [Codex CLI Documentation](https://github.com/openai/codex) -- [AgentAPI Documentation](https://github.com/coder/agentapi) - [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents) -- [AI Bridge](https://coder.com/docs/ai-coder/ai-bridge) +- [AI Gateway](https://coder.com/docs/ai-coder/ai-gateway) diff --git a/registry/coder-labs/modules/codex/main.test.ts b/registry/coder-labs/modules/codex/main.test.ts index 13055867f..a358e352a 100644 --- a/registry/coder-labs/modules/codex/main.test.ts +++ b/registry/coder-labs/modules/codex/main.test.ts @@ -6,15 +6,64 @@ import { beforeAll, expect, } from "bun:test"; -import { execContainer, readFileContainer, runTerraformInit } from "~test"; import { - loadTestFile, - writeExecutable, - setup as setupUtil, - execModuleScript, - expectAgentAPIStarted, -} from "../../../coder/modules/agentapi/test-util"; -import dedent from "dedent"; + execContainer, + readFileContainer, + removeContainer, + runContainer, + runTerraformApply, + runTerraformInit, + TerraformState, +} from "~test"; +import { extractCoderEnvVars, writeExecutable } from "../../../coder/modules/agentapi/test-util"; +import path from "path"; + +interface ModuleScripts { + pre_install?: string; + install: string; + post_install?: string; +} + +const SCRIPT_SUFFIXES = [ + "Pre-Install Script", + "Install Script", + "Post-Install Script", +] as const; + +const collectScripts = (state: TerraformState): ModuleScripts => { + const byDisplayName: Record = {}; + for (const resource of state.resources) { + if (resource.type !== "coder_script") continue; + for (const instance of resource.instances) { + const attrs = instance.attributes as Record; + const displayName = attrs.display_name as string | undefined; + const script = attrs.script as string | undefined; + if (displayName && script) { + byDisplayName[displayName] = script; + } + } + } + const scripts: Partial = {}; + for (const suffix of SCRIPT_SUFFIXES) { + const key = `Codex: ${suffix}`; + if (!(key in byDisplayName)) continue; + switch (suffix) { + case "Pre-Install Script": + scripts.pre_install = byDisplayName[key]; + break; + case "Install Script": + scripts.install = byDisplayName[key]; + break; + case "Post-Install Script": + scripts.post_install = byDisplayName[key]; + break; + } + } + if (!scripts.install) { + throw new Error("install script not found in terraform state"); + } + return scripts as ModuleScripts; +}; let cleanupFunctions: (() => Promise)[] = []; const registerCleanup = (cleanup: () => Promise) => { @@ -33,36 +82,90 @@ afterEach(async () => { }); interface SetupProps { - skipAgentAPIMock?: boolean; skipCodexMock?: boolean; moduleVariables?: Record; - agentapiMockScript?: string; } -const setup = async (props?: SetupProps): Promise<{ id: string }> => { +const setup = async ( + props?: SetupProps, +): Promise<{ + id: string; + coderEnvVars: Record; + scripts: ModuleScripts; +}> => { const projectDir = "/home/coder/project"; - const { id } = await setupUtil({ - moduleDir: import.meta.dir, - moduleVariables: { - install_codex: props?.skipCodexMock ? "true" : "false", - install_agentapi: props?.skipAgentAPIMock ? "true" : "false", - codex_model: "gpt-4-turbo", - workdir: "/home/coder", - ...props?.moduleVariables, - }, - registerCleanup, - projectDir, - skipAgentAPIMock: props?.skipAgentAPIMock, - agentapiMockScript: props?.agentapiMockScript, + const moduleDir = path.resolve(import.meta.dir); + const state = await runTerraformApply(moduleDir, { + agent_id: "foo", + workdir: projectDir, + install_codex: "false", + ...props?.moduleVariables, + }); + 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", }); if (!props?.skipCodexMock) { await writeExecutable({ containerId: id, filePath: "/usr/bin/codex", - content: await loadTestFile(import.meta.dir, "codex-mock.sh"), + content: await Bun.file( + path.join(moduleDir, "testdata", "codex-mock.sh"), + ).text(), }); } - return { id }; + return { id, coderEnvVars, scripts }; +}; + +const runScripts = async ( + id: string, + scripts: ModuleScripts, + env?: Record, +) => { + const entries = env ? Object.entries(env) : []; + const envArgs = + entries.length > 0 + ? entries + .map( + ([key, value]) => `export ${key}="${value.replace(/"/g, '\\"')}"`, + ) + .join(" && ") + " && " + : ""; + const ordered: [string, string | undefined][] = [ + ["pre_install", scripts.pre_install], + ["install", scripts.install], + ["post_install", scripts.post_install], + ]; + for (const [name, script] of ordered) { + if (!script) continue; + const target = `/tmp/coder-utils-${name}.sh`; + await writeExecutable({ + containerId: id, + filePath: target, + content: script, + }); + const resp = await execContainer(id, ["bash", "-c", `${envArgs}${target}`]); + if (resp.exitCode !== 0) { + console.log(`script ${name} failed:`); + console.log(resp.stdout); + console.log(resp.stderr); + throw new Error(`coder-utils ${name} script exited ${resp.exitCode}`); + } + } }; setDefaultTimeout(60 * 1000); @@ -73,444 +176,142 @@ describe("codex", async () => { }); test("happy-path", async () => { - const { id } = await setup(); - await execModuleScript(id); - await expectAgentAPIStarted(id); + const { id, scripts } = await setup(); + await runScripts(id, scripts); + const installLog = await readFileContainer( + id, + "/home/coder/.coder-modules/coder-labs/codex/logs/install.log", + ); + expect(installLog).toContain("Skipping Codex installation"); }); test("install-codex-version", async () => { - const version_to_install = "0.10.0"; - const { id } = await setup({ + const version = "0.10.0"; + const { id, coderEnvVars, scripts } = await setup({ skipCodexMock: true, moduleVariables: { install_codex: "true", - codex_version: version_to_install, + codex_version: version, }, }); - await execModuleScript(id); - const resp = await execContainer(id, [ - "bash", - "-c", - `cat /home/coder/.codex-module/install.log`, - ]); - expect(resp.stdout).toContain(version_to_install); + await runScripts(id, scripts, coderEnvVars); + const installLog = await readFileContainer( + id, + "/home/coder/.coder-modules/coder-labs/codex/logs/install.log", + ); + expect(installLog).toContain(version); }); - test("check-latest-codex-version-works", async () => { - const { id } = await setup({ - skipCodexMock: true, - skipAgentAPIMock: true, + test("openai-api-key", async () => { + const apiKey = "test-api-key-123"; + const { coderEnvVars } = await setup({ moduleVariables: { - install_codex: "true", + openai_api_key: apiKey, }, }); - await execModuleScript(id); - await expectAgentAPIStarted(id); + expect(coderEnvVars["OPENAI_API_KEY"]).toBe(apiKey); }); test("base-config-toml", async () => { - const baseConfig = dedent` - sandbox_mode = "danger-full-access" - approval_policy = "never" - preferred_auth_method = "apikey" - - [custom_section] - new_feature = true - `.trim(); - const { id } = await setup({ + const baseConfig = [ + 'sandbox_mode = "danger-full-access"', + 'approval_policy = "never"', + 'preferred_auth_method = "apikey"', + "", + "[custom_section]", + "new_feature = true", + ].join("\n"); + const { id, scripts } = await setup({ moduleVariables: { base_config_toml: baseConfig, }, }); - await execModuleScript(id); + await runScripts(id, scripts); const resp = await readFileContainer(id, "/home/coder/.codex/config.toml"); expect(resp).toContain('sandbox_mode = "danger-full-access"'); expect(resp).toContain('preferred_auth_method = "apikey"'); expect(resp).toContain("[custom_section]"); - expect(resp).toContain("[mcp_servers.Coder]"); - }); - - test("codex-api-key", async () => { - const apiKey = "test-api-key-123"; - const { id } = await setup({ - moduleVariables: { - openai_api_key: apiKey, - }, - }); - await execModuleScript(id); - - const resp = await readFileContainer( - id, - "/home/coder/.codex-module/agentapi-start.log", - ); - expect(resp).toContain("OpenAI API Key: Provided"); - }); - - test("pre-post-install-scripts", async () => { - const { id } = await setup({ - moduleVariables: { - pre_install_script: "#!/bin/bash\necho 'pre-install-script'", - post_install_script: "#!/bin/bash\necho 'post-install-script'", - }, - }); - await execModuleScript(id); - const preInstallLog = await readFileContainer( - id, - "/home/coder/.codex-module/pre_install.log", - ); - expect(preInstallLog).toContain("pre-install-script"); - const postInstallLog = await readFileContainer( - id, - "/home/coder/.codex-module/post_install.log", - ); - expect(postInstallLog).toContain("post-install-script"); - }); - - test("workdir-variable", async () => { - const workdir = "/tmp/codex-test-workdir"; - const { id } = await setup({ - skipCodexMock: false, - moduleVariables: { - workdir, - }, - }); - await execModuleScript(id); - const resp = await readFileContainer( - id, - "/home/coder/.codex-module/install.log", - ); - expect(resp).toContain(workdir); }); test("additional-mcp-servers", async () => { - const additional = dedent` - [mcp_servers.GitHub] - command = "npx" - args = ["-y", "@modelcontextprotocol/server-github"] - type = "stdio" - description = "GitHub integration" - - [mcp_servers.FileSystem] - command = "npx" - args = ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"] - type = "stdio" - description = "File system access" - `.trim(); - const { id } = await setup({ + const additional = [ + "[mcp_servers.GitHub]", + 'command = "npx"', + 'args = ["-y", "@modelcontextprotocol/server-github"]', + 'type = "stdio"', + 'description = "GitHub integration"', + ].join("\n"); + const { id, scripts } = await setup({ moduleVariables: { additional_mcp_servers: additional, }, }); - await execModuleScript(id); + await runScripts(id, scripts); const resp = await readFileContainer(id, "/home/coder/.codex/config.toml"); expect(resp).toContain("[mcp_servers.GitHub]"); - expect(resp).toContain("[mcp_servers.FileSystem]"); - expect(resp).toContain("[mcp_servers.Coder]"); expect(resp).toContain("GitHub integration"); }); - test("full-custom-config", async () => { - const baseConfig = dedent` - sandbox_mode = "read-only" - approval_policy = "untrusted" - preferred_auth_method = "chatgpt" - custom_setting = "test-value" - - [advanced_settings] - timeout = 30000 - debug = true - logging_level = "verbose" - `.trim(); - - const additionalMCP = dedent` - [mcp_servers.CustomTool] - command = "/usr/local/bin/custom-tool" - args = ["--serve", "--port", "8080"] - type = "stdio" - description = "Custom development tool" - - [mcp_servers.DatabaseMCP] - command = "python" - args = ["-m", "database_mcp_server"] - type = "stdio" - description = "Database query interface" - `.trim(); - - const { id } = await setup({ - moduleVariables: { - base_config_toml: baseConfig, - additional_mcp_servers: additionalMCP, - }, - }); - await execModuleScript(id); - const resp = await readFileContainer(id, "/home/coder/.codex/config.toml"); - - // Check base config - expect(resp).toContain('sandbox_mode = "read-only"'); - expect(resp).toContain('preferred_auth_method = "chatgpt"'); - expect(resp).toContain('custom_setting = "test-value"'); - expect(resp).toContain("[advanced_settings]"); - expect(resp).toContain('logging_level = "verbose"'); - - // Check MCP servers - expect(resp).toContain("[mcp_servers.Coder]"); - expect(resp).toContain("[mcp_servers.CustomTool]"); - expect(resp).toContain("[mcp_servers.DatabaseMCP]"); - expect(resp).toContain("Custom development tool"); - expect(resp).toContain("Database query interface"); - }); - test("minimal-default-config", async () => { - const { id } = await setup({ - moduleVariables: { - // No base_config_toml or additional_mcp_servers - should use defaults - }, - }); - await execModuleScript(id); + const { id, scripts } = await setup(); + await runScripts(id, scripts); const resp = await readFileContainer(id, "/home/coder/.codex/config.toml"); - - // Check default base config expect(resp).toContain('sandbox_mode = "workspace-write"'); expect(resp).toContain('approval_policy = "never"'); expect(resp).toContain("[sandbox_workspace_write]"); expect(resp).toContain("network_access = true"); - - // Check only Coder MCP server is present - expect(resp).toContain("[mcp_servers.Coder]"); - expect(resp).toContain("Report ALL tasks and statuses"); - - // Ensure no additional MCP servers - const mcpServerCount = (resp.match(/\[mcp_servers\./g) || []).length; - expect(mcpServerCount).toBe(1); }); - test("codex-system-prompt", async () => { - const prompt = "This is a system prompt for Codex."; - const { id } = await setup({ - moduleVariables: { - codex_system_prompt: prompt, - }, - }); - await execModuleScript(id); - const resp = await readFileContainer(id, "/home/coder/.codex/AGENTS.md"); - expect(resp).toContain(prompt); - }); - - test("codex-system-prompt-skip-append-if-exists", async () => { - const prompt_1 = "This is a system prompt for Codex."; - const prompt_2 = "This is a system prompt for Goose."; - const prompt_3 = dedent` - This is a system prompt for Codex. - This is a system prompt for Gemini. - `.trim(); - const pre_install_script = dedent` - #!/bin/bash - mkdir -p /home/coder/.codex - echo -e "${prompt_3}" >> /home/coder/.codex/AGENTS.md - `.trim(); - - const { id } = await setup({ + test("pre-post-install-scripts", async () => { + const { id, scripts } = await setup({ moduleVariables: { - pre_install_script, - codex_system_prompt: prompt_2, + pre_install_script: "#!/bin/bash\necho 'codex-pre-install-script'", + post_install_script: "#!/bin/bash\necho 'codex-post-install-script'", }, }); - await execModuleScript(id); - const resp = await readFileContainer(id, "/home/coder/.codex/AGENTS.md"); - expect(resp).toContain(prompt_1); - expect(resp).toContain(prompt_2); + await runScripts(id, scripts); - // Re-run with a prompt that already exists, it should not append again - const { id: id_2 } = await setup({ - moduleVariables: { - pre_install_script, - codex_system_prompt: prompt_1, - }, - }); - await execModuleScript(id_2); - const resp_2 = await readFileContainer( - id_2, - "/home/coder/.codex/AGENTS.md", + const preInstallLog = await readFileContainer( + id, + "/home/coder/.coder-modules/coder-labs/codex/logs/pre_install.log", ); - expect(resp_2).toContain(prompt_1); - const count = (resp_2.match(new RegExp(prompt_1, "g")) || []).length; - expect(count).toBe(1); - }); + expect(preInstallLog).toContain("codex-pre-install-script"); - test("codex-ai-task-prompt", async () => { - const prompt = "This is a system prompt for Codex."; - const { id } = await setup({ - moduleVariables: { - ai_prompt: prompt, - }, - }); - await execModuleScript(id); - const resp = await execContainer(id, [ - "bash", - "-c", - `cat /home/coder/.codex-module/agentapi-start.log`, - ]); - expect(resp.stdout).toContain(prompt); - }); - - test("start-without-prompt", async () => { - const { id } = await setup({ - moduleVariables: { - codex_system_prompt: "", // Explicitly disable system prompt - }, - }); - await execModuleScript(id); - const prompt = await execContainer(id, [ - "ls", - "-l", - "/home/coder/.codex/AGENTS.md", - ]); - expect(prompt.exitCode).not.toBe(0); - expect(prompt.stderr).toContain("No such file or directory"); - }); - - test("codex-continue-capture-new-session", async () => { - const { id } = await setup({ - moduleVariables: { - continue: "true", - ai_prompt: "test task", - }, - }); - - const workdir = "/home/coder"; - const expectedSessionId = "019a1234-5678-9abc-def0-123456789012"; - const sessionsDir = "/home/coder/.codex/sessions"; - const sessionFile = `${sessionsDir}/${expectedSessionId}.jsonl`; - - await execContainer(id, ["mkdir", "-p", sessionsDir]); - await execContainer(id, [ - "bash", - "-c", - `echo '{"id":"${expectedSessionId}","cwd":"${workdir}","created":"2024-10-24T20:00:00Z","model":"gpt-4-turbo"}' > ${sessionFile}`, - ]); - - await execModuleScript(id); - - await expectAgentAPIStarted(id); - - const trackingFile = "/home/coder/.codex-module/.codex-task-session"; - const maxAttempts = 30; - let trackingFileContents = ""; - for (let attempt = 0; attempt < maxAttempts; attempt++) { - const result = await execContainer(id, [ - "bash", - "-c", - `cat ${trackingFile} 2>/dev/null || echo ""`, - ]); - if (result.stdout.trim().length > 0) { - trackingFileContents = result.stdout; - break; - } - await new Promise((resolve) => setTimeout(resolve, 500)); - } - - expect(trackingFileContents).toContain(`${workdir}|${expectedSessionId}`); - - const startLog = await readFileContainer( + const postInstallLog = await readFileContainer( id, - "/home/coder/.codex-module/agentapi-start.log", + "/home/coder/.coder-modules/coder-labs/codex/logs/post_install.log", ); - expect(startLog).toContain("Capturing new session ID"); - expect(startLog).toContain("Session tracked"); - expect(startLog).toContain(expectedSessionId); + expect(postInstallLog).toContain("codex-post-install-script"); }); - test("codex-continue-resume-existing-session", async () => { - const { id } = await setup({ + test("workdir-variable", async () => { + const workdir = "/home/coder/codex-test-folder"; + const { id, scripts } = await setup({ moduleVariables: { - continue: "true", - ai_prompt: "test prompt", + workdir, }, }); - - const workdir = "/home/coder"; - const mockSessionId = "019a1234-5678-9abc-def0-123456789012"; - const trackingFile = "/home/coder/.codex-module/.codex-task-session"; - - await execContainer(id, ["mkdir", "-p", "/home/coder/.codex-module"]); - await execContainer(id, [ - "bash", - "-c", - `echo "${workdir}|${mockSessionId}" > ${trackingFile}`, - ]); - - await execModuleScript(id); - - const startLog = await execContainer(id, [ - "bash", - "-c", - "cat /home/coder/.codex-module/agentapi-start.log", - ]); - expect(startLog.stdout).toContain("Found existing task session"); - expect(startLog.stdout).toContain(mockSessionId); - expect(startLog.stdout).toContain("Resuming existing session"); - expect(startLog.stdout).toContain( - `Starting Codex with arguments: --model gpt-4-turbo resume ${mockSessionId}`, + await runScripts(id, scripts); + const installLog = await readFileContainer( + id, + "/home/coder/.coder-modules/coder-labs/codex/logs/install.log", ); - expect(startLog.stdout).not.toContain("test prompt"); + expect(installLog).toContain(workdir); }); - test("codex-with-aibridge", async () => { - const { id } = await setup({ + test("codex-with-ai-gateway", async () => { + const { id, coderEnvVars, scripts } = await setup({ moduleVariables: { - enable_aibridge: "true", + enable_ai_gateway: "true", model_reasoning_effort: "none", }, }); - - await execModuleScript(id); + await runScripts(id, scripts, coderEnvVars); const configToml = await readFileContainer( id, "/home/coder/.codex/config.toml", ); expect(configToml).toContain('model_provider = "aibridge"'); - }); - - test("boundary-enabled", async () => { - const { id } = await setup({ - moduleVariables: { - enable_boundary: "true", - boundary_config_path: "/tmp/test-boundary.yaml", - }, - }); - // Write boundary config - await execContainer(id, [ - "bash", - "-c", - `cat > /tmp/test-boundary.yaml <<'EOF' -jail_type: landjail -proxy_port: 8087 -log_level: warn -allowlist: - - "domain=api.openai.com" -EOF`, - ]); - // Add mock coder binary for boundary setup - await writeExecutable({ - containerId: id, - filePath: "/usr/bin/coder", - content: `#!/bin/bash -if [ "$1" = "boundary" ]; then - if [ "$2" = "--help" ]; then - echo "boundary help" - exit 0 - fi - shift; shift; exec "$@" -fi -echo "mock coder"`, - }); - await execModuleScript(id); - await expectAgentAPIStarted(id); - // Verify boundary wrapper was used in start script - const startLog = await readFileContainer( - id, - "/home/coder/.codex-module/agentapi-start.log", - ); - expect(startLog).toContain("boundary"); + expect(configToml).toContain("[model_providers.aibridge]"); }); }); diff --git a/registry/coder-labs/modules/codex/main.tf b/registry/coder-labs/modules/codex/main.tf index b5f71cb3c..364b4e4c8 100644 --- a/registry/coder-labs/modules/codex/main.tf +++ b/registry/coder-labs/modules/codex/main.tf @@ -18,18 +18,6 @@ data "coder_workspace" "me" {} data "coder_workspace_owner" "me" {} -variable "order" { - type = number - description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)." - default = null -} - -variable "group" { - type = string - description = "The name of a group that this app belongs to." - default = null -} - variable "icon" { type = string description = "The icon to use for the app." @@ -38,58 +26,20 @@ variable "icon" { variable "workdir" { type = string - description = "The folder to run Codex in." -} - -variable "report_tasks" { - type = bool - description = "Whether to enable task reporting to Coder UI via AgentAPI" - default = true -} - -variable "subdomain" { - type = bool - description = "Whether to use a subdomain for AgentAPI." - default = false -} - -variable "cli_app" { - type = bool - description = "Whether to create a CLI app for Codex" - default = false -} - -variable "web_app_display_name" { - type = string - description = "Display name for the web app" - default = "Codex" + description = "Optional project directory. When set, the module pre-creates it if missing and adds it as a trusted project in Codex config.toml." + default = null } -variable "cli_app_display_name" { +variable "pre_install_script" { type = string - description = "Display name for the CLI app" - default = "Codex CLI" -} - -variable "enable_aibridge" { - type = bool - description = "Use AI Bridge for Codex. https://coder.com/docs/ai-coder/ai-bridge" - default = false - - validation { - condition = !(var.enable_aibridge && length(var.openai_api_key) > 0) - error_message = "openai_api_key cannot be provided when enable_aibridge is true. AI Bridge automatically authenticates the client using Coder credentials." - } + description = "Custom script to run before installing Codex." + default = null } -variable "model_reasoning_effort" { +variable "post_install_script" { type = string - description = "The reasoning effort for the model. One of: none, low, medium, high. https://platform.openai.com/docs/guides/latest-model#lower-reasoning-effort" - default = "" - validation { - condition = contains(["", "none", "minimal", "low", "medium", "high", "xhigh"], var.model_reasoning_effort) - error_message = "model_reasoning_effort must be one of: none, low, medium, high." - } + description = "Custom script to run after installing Codex." + default = null } variable "install_codex" { @@ -101,130 +51,72 @@ variable "install_codex" { variable "codex_version" { type = string description = "The version of Codex to install." - default = "" # empty string means the latest available version -} - -variable "base_config_toml" { - type = string - description = "Complete base TOML configuration for Codex (without mcp_servers section). If empty, uses minimal default configuration with workspace-write sandbox mode and never approval policy. For advanced options, see https://github.com/openai/codex/blob/main/codex-rs/config.md" - default = "" -} - -variable "additional_mcp_servers" { - type = string - description = "Additional MCP servers configuration in TOML format. These will be merged with the required Coder MCP server in the [mcp_servers] section." default = "" } variable "openai_api_key" { type = string - description = "OpenAI API key for Codex CLI" + description = "OpenAI API key for Codex CLI." default = "" } -variable "install_agentapi" { - type = bool - description = "Whether to install AgentAPI." - default = true -} - -variable "agentapi_version" { - type = string - description = "The version of AgentAPI to install." - default = "v0.12.1" -} - variable "codex_model" { type = string - description = "The model for Codex to use. Defaults to gpt-5.3-codex." + description = "The model for Codex to use." default = "gpt-5.4" } -variable "pre_install_script" { - type = string - description = "Custom script to run before installing Codex." - default = null -} - -variable "post_install_script" { - type = string - description = "Custom script to run after installing Codex." - default = null -} - -variable "ai_prompt" { +variable "base_config_toml" { type = string - description = "Initial task prompt for Codex CLI when launched via Tasks" + description = "Complete base TOML configuration for Codex (without mcp_servers section). If empty, uses minimal default configuration with workspace-write sandbox mode and never approval policy." default = "" } -variable "continue" { - type = bool - description = "Automatically continue existing sessions on workspace restart. When true, resumes existing conversation if found, otherwise runs prompt or starts new session. When false, always starts fresh (ignores existing sessions)." - default = true -} - -variable "enable_state_persistence" { - type = bool - description = "Enable AgentAPI conversation state persistence across restarts." - default = true -} - -variable "codex_system_prompt" { - type = string - description = "System instructions written to AGENTS.md in the ~/.codex directory" - default = "You are a helpful coding assistant. Start every response with `Codex says:`" -} - -variable "enable_boundary" { - type = bool - description = "Enable coder boundary for network filtering." - default = false -} - -variable "boundary_config_path" { +variable "additional_mcp_servers" { type = string - description = "Path to boundary config.yaml inside the workspace. If provided, exposed as BOUNDARY_CONFIG env var." + description = "Additional MCP servers configuration in TOML format." default = "" } -variable "boundary_version" { +variable "model_reasoning_effort" { type = string - description = "Boundary version. When use_boundary_directly is true, a release version should be provided or 'latest' for the latest release." - default = "latest" + description = "The reasoning effort for the model." + default = "" + validation { + condition = contains(["", "none", "minimal", "low", "medium", "high", "xhigh"], var.model_reasoning_effort) + error_message = "model_reasoning_effort must be one of: none, minimal, low, medium, high, xhigh." + } } -variable "compile_boundary_from_source" { +variable "enable_ai_gateway" { type = bool - description = "Whether to compile boundary from source instead of using the official install script." + description = "Use AI Gateway for Codex. https://coder.com/docs/ai-coder/ai-gateway" default = false -} -variable "use_boundary_directly" { - type = bool - description = "Whether to use boundary binary directly instead of coder boundary subcommand." - default = false + validation { + condition = !(var.enable_ai_gateway && length(var.openai_api_key) > 0) + error_message = "openai_api_key cannot be provided when enable_ai_gateway is true. AI Gateway automatically authenticates the client using Coder credentials." + } } resource "coder_env" "openai_api_key" { + count = var.openai_api_key != "" ? 1 : 0 agent_id = var.agent_id name = "OPENAI_API_KEY" value = var.openai_api_key } -resource "coder_env" "coder_aibridge_session_token" { - count = var.enable_aibridge ? 1 : 0 +# Authenticates the client against Coder's AI Gateway using the workspace +# owner's session token. Referenced by config.toml model_providers.aibridge. +resource "coder_env" "ai_gateway_session_token" { + count = var.enable_ai_gateway ? 1 : 0 agent_id = var.agent_id name = "CODER_AIBRIDGE_SESSION_TOKEN" value = data.coder_workspace_owner.me.session_token } locals { - workdir = trimsuffix(var.workdir, "/") - app_slug = "codex" - install_script = file("${path.module}/scripts/install.sh") - start_script = file("${path.module}/scripts/start.sh") - module_dir_name = ".codex-module" + workdir = var.workdir != null ? trimsuffix(var.workdir, "/") : "" latest_codex_model = "gpt-5.4" aibridge_config = <<-EOF [model_providers.aibridge] @@ -234,76 +126,35 @@ locals { wire_api = "responses" EOF -} - -module "agentapi" { - source = "registry.coder.com/coder/agentapi/coder" - version = "2.3.0" - - agent_id = var.agent_id - folder = local.workdir - web_app_slug = local.app_slug - web_app_order = var.order - web_app_group = var.group - web_app_icon = var.icon - web_app_display_name = var.web_app_display_name - cli_app = var.cli_app - cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null - cli_app_display_name = var.cli_app ? var.cli_app_display_name : null - module_dir_name = local.module_dir_name - install_agentapi = var.install_agentapi - agentapi_subdomain = var.subdomain - agentapi_version = var.agentapi_version - enable_state_persistence = var.enable_state_persistence - pre_install_script = var.pre_install_script - post_install_script = var.post_install_script - enable_boundary = var.enable_boundary - boundary_config_path = var.boundary_config_path - boundary_version = var.boundary_version - compile_boundary_from_source = var.compile_boundary_from_source - use_boundary_directly = var.use_boundary_directly - start_script = <<-EOT - #!/bin/bash - set -o errexit - set -o pipefail - - echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh - chmod +x /tmp/start.sh - ARG_OPENAI_API_KEY='${var.openai_api_key}' \ - ARG_REPORT_TASKS='${var.report_tasks}' \ - ARG_CODEX_MODEL='${var.codex_model}' \ - ARG_CODEX_START_DIRECTORY='${local.workdir}' \ - ARG_CODEX_TASK_PROMPT='${base64encode(var.ai_prompt)}' \ - ARG_CONTINUE='${var.continue}' \ - ARG_ENABLE_AIBRIDGE='${var.enable_aibridge}' \ - /tmp/start.sh - EOT - - install_script = <<-EOT - #!/bin/bash - set -o errexit - set -o pipefail - - echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh - chmod +x /tmp/install.sh - ARG_OPENAI_API_KEY='${var.openai_api_key}' \ - ARG_REPORT_TASKS='${var.report_tasks}' \ - ARG_CODEX_MODEL='${var.codex_model}' \ - ARG_LATEST_CODEX_MODEL='${local.latest_codex_model}' \ - ARG_INSTALL='${var.install_codex}' \ - ARG_CODEX_VERSION='${var.codex_version}' \ - ARG_BASE_CONFIG_TOML='${base64encode(var.base_config_toml)}' \ - ARG_ENABLE_AIBRIDGE='${var.enable_aibridge}' \ - ARG_AIBRIDGE_CONFIG='${base64encode(var.enable_aibridge ? local.aibridge_config : "")}' \ - ARG_ADDITIONAL_MCP_SERVERS='${base64encode(var.additional_mcp_servers)}' \ - ARG_CODER_MCP_APP_STATUS_SLUG='${local.app_slug}' \ - ARG_CODEX_START_DIRECTORY='${local.workdir}' \ - ARG_MODEL_REASONING_EFFORT='${var.model_reasoning_effort}' \ - ARG_CODEX_INSTRUCTION_PROMPT='${base64encode(var.codex_system_prompt)}' \ - /tmp/install.sh - EOT -} - -output "task_app_id" { - value = module.agentapi.task_app_id + install_script = templatefile("${path.module}/scripts/install.sh.tftpl", { + ARG_INSTALL = tostring(var.install_codex) + ARG_CODEX_VERSION = var.codex_version + ARG_WORKDIR = local.workdir + ARG_CODEX_MODEL = var.codex_model + ARG_LATEST_CODEX_MODEL = local.latest_codex_model + ARG_BASE_CONFIG_TOML = var.base_config_toml != "" ? base64encode(var.base_config_toml) : "" + ARG_ADDITIONAL_MCP_SERVERS = var.additional_mcp_servers != "" ? base64encode(var.additional_mcp_servers) : "" + 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 + }) + module_dir_name = ".coder-modules/coder-labs/codex" +} + +module "coder_utils" { + source = "registry.coder.com/coder/coder-utils/coder" + version = "0.0.1" + + agent_id = var.agent_id + module_directory = "$HOME/${local.module_dir_name}" + display_name_prefix = "Codex" + icon = var.icon + pre_install_script = var.pre_install_script + post_install_script = var.post_install_script + install_script = local.install_script +} + +output "scripts" { + description = "Ordered list of coder exp sync names for the coder_script resources this module creates, in run order (pre_install, install, post_install). Scripts that were not configured are absent from the list." + value = module.coder_utils.scripts } diff --git a/registry/coder-labs/modules/codex/main.tftest.hcl b/registry/coder-labs/modules/codex/main.tftest.hcl index 1237df5de..b41cd6717 100644 --- a/registry/coder-labs/modules/codex/main.tftest.hcl +++ b/registry/coder-labs/modules/codex/main.tftest.hcl @@ -2,14 +2,8 @@ run "test_codex_basic" { command = plan variables { - agent_id = "test-agent" - workdir = "/home/coder" - openai_api_key = "test-key" - } - - assert { - condition = var.agent_id == "test-agent" - error_message = "Agent ID should be set correctly" + agent_id = "test-agent" + workdir = "/home/coder" } assert { @@ -21,167 +15,192 @@ run "test_codex_basic" { condition = var.install_codex == true error_message = "install_codex should default to true" } +} - assert { - condition = var.install_agentapi == true - error_message = "install_agentapi should default to true" - } +run "test_codex_with_api_key" { + command = plan - assert { - condition = var.report_tasks == true - error_message = "report_tasks should default to true" + variables { + agent_id = "test-agent" + workdir = "/home/coder" + openai_api_key = "test-key" } assert { - condition = var.continue == true - error_message = "continue should default to true" + condition = coder_env.openai_api_key[0].value == "test-key" + error_message = "OpenAI API key should be set correctly" } } -run "test_enable_state_persistence_default" { +run "test_codex_custom_options" { command = plan variables { - agent_id = "test-agent" - workdir = "/home/coder" - openai_api_key = "test-key" + agent_id = "test-agent" + workdir = "/home/coder/project" + icon = "/icon/custom.svg" + codex_model = "gpt-4o" + codex_version = "0.1.0" } assert { - condition = var.enable_state_persistence == true - error_message = "enable_state_persistence should default to true" + condition = var.icon == "/icon/custom.svg" + error_message = "Icon should be set to custom icon" + } + + assert { + condition = var.codex_model == "gpt-4o" + error_message = "codex_model should be set to 'gpt-4o'" } } -run "test_disable_state_persistence" { +run "test_ai_gateway_enabled" { command = plan variables { - agent_id = "test-agent" - workdir = "/home/coder" - openai_api_key = "test-key" - enable_state_persistence = false + agent_id = "test-agent" + workdir = "/home/coder" + enable_ai_gateway = true + } + + override_data { + target = data.coder_workspace_owner.me + values = { + session_token = "mock-session-token" + } + } + + assert { + condition = var.enable_ai_gateway == true + error_message = "AI Gateway should be enabled" } assert { - condition = var.enable_state_persistence == false - error_message = "enable_state_persistence should be false when explicitly disabled" + condition = coder_env.ai_gateway_session_token[0].name == "CODER_AIBRIDGE_SESSION_TOKEN" + error_message = "CODER_AIBRIDGE_SESSION_TOKEN should be set" + } + + assert { + condition = coder_env.ai_gateway_session_token[0].value == data.coder_workspace_owner.me.session_token + error_message = "Session token should use workspace owner's token" + } + + assert { + condition = length(coder_env.openai_api_key) == 0 + error_message = "OPENAI_API_KEY should not be created when ai_gateway is enabled" } } -run "test_codex_with_aibridge" { +run "test_ai_gateway_validation_with_api_key" { command = plan variables { - agent_id = "test-agent" - workdir = "/home/coder" - enable_aibridge = true + agent_id = "test-agent" + workdir = "/home/coder" + enable_ai_gateway = true + openai_api_key = "test-key" } - assert { - condition = var.enable_aibridge == true - error_message = "enable_aibridge should be set to true" - } + expect_failures = [ + var.enable_ai_gateway, + ] } -run "test_aibridge_disabled_with_api_key" { +run "test_ai_gateway_disabled_with_api_key" { command = plan variables { - agent_id = "test-agent" - workdir = "/home/coder" - openai_api_key = "test-key" - enable_aibridge = false + agent_id = "test-agent" + workdir = "/home/coder" + enable_ai_gateway = false + openai_api_key = "test-key-xyz" } assert { - condition = var.enable_aibridge == false - error_message = "enable_aibridge should be false" + condition = coder_env.openai_api_key[0].value == "test-key-xyz" + error_message = "OPENAI_API_KEY should use the provided API key" } assert { - condition = coder_env.openai_api_key.value == "test-key" - error_message = "OpenAI API key should be set correctly" + condition = length(coder_env.ai_gateway_session_token) == 0 + error_message = "Session token should not be set when ai_gateway is disabled" } } -run "test_custom_options" { +run "test_no_api_key_no_env" { command = plan variables { - agent_id = "test-agent" - workdir = "/home/coder/project" - openai_api_key = "test-key" - order = 5 - group = "ai-tools" - icon = "/icon/custom.svg" - web_app_display_name = "Custom Codex" - cli_app = true - cli_app_display_name = "Codex Terminal" - subdomain = true - report_tasks = false - continue = false - codex_model = "gpt-4o" - codex_version = "0.1.0" - agentapi_version = "v0.12.0" + agent_id = "test-agent" + workdir = "/home/coder" } assert { - condition = var.order == 5 - error_message = "Order should be set to 5" + condition = length(coder_env.openai_api_key) == 0 + error_message = "OPENAI_API_KEY should not be created when no API key is provided" } +} - assert { - condition = var.group == "ai-tools" - error_message = "Group should be set to 'ai-tools'" +run "test_codex_with_scripts" { + command = plan + + variables { + agent_id = "test-agent" + workdir = "/home/coder" + pre_install_script = "echo 'Pre-install script'" + post_install_script = "echo 'Post-install script'" } assert { - condition = var.icon == "/icon/custom.svg" - error_message = "Icon should be set to custom icon" + condition = var.pre_install_script == "echo 'Pre-install script'" + error_message = "Pre-install script should be set correctly" } assert { - condition = var.cli_app == true - error_message = "cli_app should be enabled" + condition = var.post_install_script == "echo 'Post-install script'" + error_message = "Post-install script should be set correctly" } +} - assert { - condition = var.subdomain == true - error_message = "subdomain should be enabled" +run "test_script_outputs_install_only" { + command = plan + + variables { + agent_id = "test-agent" + workdir = "/home/coder" } assert { - condition = var.report_tasks == false - error_message = "report_tasks should be disabled" + condition = length(output.scripts) == 1 && output.scripts[0] == "coder-labs-codex-install_script" + error_message = "scripts output should list only the install script when pre/post are not configured" } +} - assert { - condition = var.continue == false - error_message = "continue should be disabled" +run "test_script_outputs_with_pre_and_post" { + command = plan + + variables { + agent_id = "test-agent" + workdir = "/home/coder" + pre_install_script = "echo pre" + post_install_script = "echo post" } assert { - condition = var.codex_model == "gpt-4o" - error_message = "codex_model should be set to 'gpt-4o'" + condition = output.scripts == ["coder-labs-codex-pre_install_script", "coder-labs-codex-install_script", "coder-labs-codex-post_install_script"] + error_message = "scripts output should list pre_install, install, post_install in run order" } } -run "test_no_api_key_no_aibridge" { +run "test_workdir_optional" { command = plan variables { agent_id = "test-agent" - workdir = "/home/coder" - } - - assert { - condition = var.openai_api_key == "" - error_message = "openai_api_key should be empty when not provided" } assert { - condition = var.enable_aibridge == false - error_message = "enable_aibridge should default to false" + condition = var.workdir == null + error_message = "workdir should default to null when omitted" } } diff --git a/registry/coder-labs/modules/codex/scripts/install.sh b/registry/coder-labs/modules/codex/scripts/install.sh deleted file mode 100644 index 9a191a024..000000000 --- a/registry/coder-labs/modules/codex/scripts/install.sh +++ /dev/null @@ -1,228 +0,0 @@ -#!/bin/bash -source "$HOME"/.bashrc - -BOLD='\033[0;1m' - -command_exists() { - command -v "$1" > /dev/null 2>&1 -} -set -o errexit -set -o pipefail -set -o nounset - -ARG_BASE_CONFIG_TOML=$(echo -n "$ARG_BASE_CONFIG_TOML" | base64 -d) -ARG_ADDITIONAL_MCP_SERVERS=$(echo -n "$ARG_ADDITIONAL_MCP_SERVERS" | base64 -d) -ARG_CODEX_INSTRUCTION_PROMPT=$(echo -n "$ARG_CODEX_INSTRUCTION_PROMPT" | base64 -d) -ARG_ENABLE_AIBRIDGE=${ARG_ENABLE_AIBRIDGE:-false} -ARG_AIBRIDGE_CONFIG=$(echo -n "$ARG_AIBRIDGE_CONFIG" | base64 -d) - -echo "=== Codex Module Configuration ===" -printf "Install Codex: %s\n" "$ARG_INSTALL" -printf "Codex Version: %s\n" "$ARG_CODEX_VERSION" -printf "App Slug: %s\n" "$ARG_CODER_MCP_APP_STATUS_SLUG" -printf "Codex Model: %s\n" "${ARG_CODEX_MODEL:-"Default"}" -printf "Latest Codex Model: %s\n" "${ARG_LATEST_CODEX_MODEL}" -printf "Start Directory: %s\n" "$ARG_CODEX_START_DIRECTORY" -printf "Has Base Config: %s\n" "$([ -n "$ARG_BASE_CONFIG_TOML" ] && echo "Yes" || echo "No")" -printf "Has Additional MCP: %s\n" "$([ -n "$ARG_ADDITIONAL_MCP_SERVERS" ] && echo "Yes" || echo "No")" -printf "Has System Prompt: %s\n" "$([ -n "$ARG_CODEX_INSTRUCTION_PROMPT" ] && echo "Yes" || echo "No")" -printf "OpenAI API Key: %s\n" "$([ -n "$ARG_OPENAI_API_KEY" ] && echo "Provided" || echo "Not provided")" -printf "Report Tasks: %s\n" "$ARG_REPORT_TASKS" -printf "Enable Coder AI Bridge: %s\n" "$ARG_ENABLE_AIBRIDGE" -echo "======================================" - -set +o nounset - -function install_node() { - if ! command_exists npm; then - printf "npm not found, checking for Node.js installation...\n" - if ! command_exists node; then - printf "Node.js not found, installing Node.js via NVM...\n" - export NVM_DIR="$HOME/.nvm" - if [ ! -d "$NVM_DIR" ]; then - mkdir -p "$NVM_DIR" - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash - [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" - else - [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" - fi - - nvm install --lts - nvm use --lts - nvm alias default node - - printf "Node.js installed: %s\n" "$(node --version)" - printf "npm installed: %s\n" "$(npm --version)" - else - printf "Node.js is installed but npm is not available. Please install npm manually.\n" - exit 1 - fi - fi -} - -function install_codex() { - if [ "${ARG_INSTALL}" = "true" ]; then - install_node - - if ! command_exists nvm; then - printf "which node: %s\n" "$(which node)" - printf "which npm: %s\n" "$(which npm)" - - mkdir -p "$HOME"/.npm-global - - npm config set prefix "$HOME/.npm-global" - - export PATH="$HOME/.npm-global/bin:$PATH" - - if ! grep -q "export PATH=$HOME/.npm-global/bin:\$PATH" ~/.bashrc; then - echo "export PATH=$HOME/.npm-global/bin:\$PATH" >> ~/.bashrc - fi - fi - - printf "%s Installing Codex CLI\n" "${BOLD}" - - if [ -n "$ARG_CODEX_VERSION" ]; then - npm install -g "@openai/codex@$ARG_CODEX_VERSION" - else - npm install -g "@openai/codex" - fi - printf "%s Successfully installed Codex CLI. Version: %s\n" "${BOLD}" "$(codex --version)" - fi -} - -write_minimal_default_config() { - local config_path="$1" - - ARG_OPTIONAL_TOP_LEVEL_CONFIG="" - - if [[ "${ARG_ENABLE_AIBRIDGE}" = "true" ]]; then - ARG_OPTIONAL_TOP_LEVEL_CONFIG='model_provider = "aibridge"' - fi - - if [[ "${ARG_MODEL_REASONING_EFFORT}" != "" ]]; then - ARG_OPTIONAL_TOP_LEVEL_CONFIG+=$'\n'"model_reasoning_effort = \"${ARG_MODEL_REASONING_EFFORT}\"" - fi - - cat << EOF > "$config_path" -# Minimal Default Codex Configuration -sandbox_mode = "workspace-write" -approval_policy = "never" -preferred_auth_method = "apikey" -${ARG_OPTIONAL_TOP_LEVEL_CONFIG} - -[sandbox_workspace_write] -network_access = true - -[notice.model_migrations] -"${ARG_CODEX_MODEL}" = "${ARG_LATEST_CODEX_MODEL}" - -[projects."${ARG_CODEX_START_DIRECTORY}"] -trust_level = "trusted" - -EOF -} - -append_mcp_servers_section() { - local config_path="$1" - - if [ "${ARG_REPORT_TASKS}" == "false" ]; then - ARG_CODER_MCP_APP_STATUS_SLUG="" - CODER_MCP_AI_AGENTAPI_URL="" - else - CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284" - fi - - cat << EOF >> "$config_path" - -# MCP Servers Configuration -[mcp_servers.Coder] -command = "coder" -args = ["exp", "mcp", "server"] -env = { "CODER_MCP_APP_STATUS_SLUG" = "${ARG_CODER_MCP_APP_STATUS_SLUG}", "CODER_MCP_AI_AGENTAPI_URL" = "${CODER_MCP_AI_AGENTAPI_URL}" , "CODER_AGENT_URL" = "${CODER_AGENT_URL}", "CODER_AGENT_TOKEN" = "${CODER_AGENT_TOKEN}", "CODER_MCP_ALLOWED_TOOLS" = "coder_report_task" } -description = "Report ALL tasks and statuses (in progress, done, failed) you are working on." -type = "stdio" - -EOF - - if [ -n "$ARG_ADDITIONAL_MCP_SERVERS" ]; then - printf "Adding additional MCP servers\n" - echo "$ARG_ADDITIONAL_MCP_SERVERS" >> "$config_path" - fi -} - -append_aibridge_config_section() { - local config_path="$1" - - if [ -n "$ARG_AIBRIDGE_CONFIG" ]; then - printf "Adding AI Bridge configuration\n" - echo -e "\n# AI Bridge Configuration\n$ARG_AIBRIDGE_CONFIG" >> "$config_path" - fi -} - -function populate_config_toml() { - CONFIG_PATH="$HOME/.codex/config.toml" - mkdir -p "$(dirname "$CONFIG_PATH")" - - if [ -n "$ARG_BASE_CONFIG_TOML" ]; then - printf "Using provided base configuration\n" - echo "$ARG_BASE_CONFIG_TOML" > "$CONFIG_PATH" - else - printf "Using minimal default configuration\n" - write_minimal_default_config "$CONFIG_PATH" - fi - - append_mcp_servers_section "$CONFIG_PATH" - - if [ "$ARG_ENABLE_AIBRIDGE" = "true" ]; then - printf "AI Bridge is enabled\n" - append_aibridge_config_section "$CONFIG_PATH" - fi -} - -function add_instruction_prompt_if_exists() { - if [ -n "${ARG_CODEX_INSTRUCTION_PROMPT:-}" ]; then - AGENTS_PATH="$HOME/.codex/AGENTS.md" - printf "Creating AGENTS.md in .codex directory: %s\\n" "${AGENTS_PATH}" - - mkdir -p "$HOME/.codex" - - if [ -f "${AGENTS_PATH}" ] && grep -Fq "${ARG_CODEX_INSTRUCTION_PROMPT}" "${AGENTS_PATH}"; then - printf "AGENTS.md already contains the instruction prompt. Skipping append.\n" - else - printf "Appending instruction prompt to AGENTS.md in .codex directory\n" - echo -e "\n${ARG_CODEX_INSTRUCTION_PROMPT}" >> "${AGENTS_PATH}" - fi - - if [ ! -d "${ARG_CODEX_START_DIRECTORY}" ]; then - printf "Creating start directory '%s'\\n" "${ARG_CODEX_START_DIRECTORY}" - mkdir -p "${ARG_CODEX_START_DIRECTORY}" || { - printf "Error: Could not create directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}" - exit 1 - } - fi - else - printf "AGENTS.md instruction prompt is not set.\n" - fi -} - -function add_auth_json() { - AUTH_JSON_PATH="$HOME/.codex/auth.json" - mkdir -p "$(dirname "$AUTH_JSON_PATH")" - AUTH_JSON=$( - cat << EOF -{ - "OPENAI_API_KEY": "${ARG_OPENAI_API_KEY}" -} -EOF - ) - echo "$AUTH_JSON" > "$AUTH_JSON_PATH" -} - -install_codex -codex --version -populate_config_toml -add_instruction_prompt_if_exists - -if [ "$ARG_ENABLE_AIBRIDGE" = "false" ]; then - add_auth_json -fi diff --git a/registry/coder-labs/modules/codex/scripts/install.sh.tftpl b/registry/coder-labs/modules/codex/scripts/install.sh.tftpl new file mode 100644 index 000000000..b5ab0ef9e --- /dev/null +++ b/registry/coder-labs/modules/codex/scripts/install.sh.tftpl @@ -0,0 +1,152 @@ +#!/bin/bash + +set -euo pipefail + +BOLD='\033[0;1m' + +command_exists() { + command -v "$1" > /dev/null 2>&1 +} + +ARG_INSTALL='${ARG_INSTALL}' +ARG_CODEX_VERSION='${ARG_CODEX_VERSION}' +ARG_WORKDIR='${ARG_WORKDIR}' +ARG_CODEX_MODEL='${ARG_CODEX_MODEL}' +ARG_LATEST_CODEX_MODEL='${ARG_LATEST_CODEX_MODEL}' +ARG_BASE_CONFIG_TOML=$(echo -n '${ARG_BASE_CONFIG_TOML}' | base64 -d) +ARG_ADDITIONAL_MCP_SERVERS=$(echo -n '${ARG_ADDITIONAL_MCP_SERVERS}' | 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}' + +echo "--------------------------------" +printf "ARG_INSTALL: %s\n" "$${ARG_INSTALL}" +printf "ARG_CODEX_VERSION: %s\n" "$${ARG_CODEX_VERSION}" +printf "ARG_WORKDIR: %s\n" "$${ARG_WORKDIR}" +printf "ARG_CODEX_MODEL: %s\n" "$${ARG_CODEX_MODEL}" +printf "ARG_ENABLE_AI_GATEWAY: %s\n" "$${ARG_ENABLE_AI_GATEWAY}" +echo "--------------------------------" + +function install_node() { + if ! command_exists npm; then + printf "npm not found, checking for Node.js installation...\n" + if ! command_exists node; then + printf "Node.js not found, installing Node.js via NVM...\n" + export NVM_DIR="$HOME/.nvm" + if [ ! -d "$NVM_DIR" ]; then + mkdir -p "$NVM_DIR" + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + else + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + fi + + nvm install --lts + nvm use --lts + nvm alias default node + + printf "Node.js installed: %s\n" "$(node --version)" + printf "npm installed: %s\n" "$(npm --version)" + else + printf "Node.js is installed but npm is not available.\n" + exit 1 + fi + fi +} + +function install_codex() { + if [ "$${ARG_INSTALL}" != "true" ]; then + echo "Skipping Codex installation as per configuration." + return + fi + + install_node + + if ! command_exists nvm; then + mkdir -p "$HOME"/.npm-global + npm config set prefix "$HOME/.npm-global" + export PATH="$HOME/.npm-global/bin:$PATH" + + if ! grep -q "export PATH=$HOME/.npm-global/bin:\$PATH" ~/.bashrc 2>/dev/null; then + echo "export PATH=$HOME/.npm-global/bin:\$PATH" >> ~/.bashrc + fi + fi + + printf "%s Installing Codex CLI\n" "$${BOLD}" + + if [ -n "$${ARG_CODEX_VERSION}" ]; then + npm install -g "@openai/codex@$${ARG_CODEX_VERSION}" + else + npm install -g "@openai/codex" + fi + printf "%s Installed Codex CLI: %s\n" "$${BOLD}" "$(codex --version)" +} + +function write_minimal_default_config() { + local config_path="$1" + local optional_config="" + + if [ "$${ARG_ENABLE_AI_GATEWAY}" = "true" ]; then + optional_config='model_provider = "aibridge"' + fi + + if [ -n "$${ARG_MODEL_REASONING_EFFORT}" ]; then + optional_config+=$'\n'"model_reasoning_effort = \"$${ARG_MODEL_REASONING_EFFORT}\"" + fi + + cat << EOF > "$${config_path}" +sandbox_mode = "workspace-write" +approval_policy = "never" +preferred_auth_method = "apikey" +$${optional_config} + +[sandbox_workspace_write] +network_access = true + +[notice.model_migrations] +"$${ARG_CODEX_MODEL}" = "$${ARG_LATEST_CODEX_MODEL}" + +EOF + + if [ -n "$${ARG_WORKDIR}" ]; then + cat << EOF >> "$${config_path}" +[projects."$${ARG_WORKDIR}"] +trust_level = "trusted" + +EOF + fi +} + +function populate_config_toml() { + local config_path="$HOME/.codex/config.toml" + mkdir -p "$(dirname "$${config_path}")" + + if [ -n "$${ARG_BASE_CONFIG_TOML}" ]; then + printf "Using provided base configuration\n" + echo "$${ARG_BASE_CONFIG_TOML}" > "$${config_path}" + else + printf "Using minimal default configuration\n" + write_minimal_default_config "$${config_path}" + fi + + if [ -n "$${ARG_ADDITIONAL_MCP_SERVERS}" ]; then + printf "Adding additional MCP servers\n" + echo "$${ARG_ADDITIONAL_MCP_SERVERS}" >> "$${config_path}" + fi + + if [ "$${ARG_ENABLE_AI_GATEWAY}" = "true" ] && [ -n "$${ARG_AIBRIDGE_CONFIG}" ]; then + printf "Adding AI Gateway configuration\n" + echo -e "\n$${ARG_AIBRIDGE_CONFIG}" >> "$${config_path}" + fi +} + +function setup_workdir() { + if [ -n "$${ARG_WORKDIR}" ] && [ ! -d "$${ARG_WORKDIR}" ]; then + echo "Creating workdir: $${ARG_WORKDIR}" + mkdir -p "$${ARG_WORKDIR}" + fi +} + +install_codex +populate_config_toml +setup_workdir diff --git a/registry/coder-labs/modules/codex/scripts/start.sh b/registry/coder-labs/modules/codex/scripts/start.sh deleted file mode 100644 index bac0cb459..000000000 --- a/registry/coder-labs/modules/codex/scripts/start.sh +++ /dev/null @@ -1,229 +0,0 @@ -#!/bin/bash - -source "$HOME"/.bashrc -set -o errexit -set -o pipefail - -command_exists() { - command -v "$1" > /dev/null 2>&1 -} - -if [ -f "$HOME/.nvm/nvm.sh" ]; then - source "$HOME"/.nvm/nvm.sh -else - export PATH="$HOME/.npm-global/bin:$PATH" -fi - -printf "Version: %s\n" "$(codex --version)" -set -o nounset -ARG_CODEX_TASK_PROMPT=$(echo -n "$ARG_CODEX_TASK_PROMPT" | base64 -d) -ARG_CONTINUE=${ARG_CONTINUE:-true} -ARG_ENABLE_AIBRIDGE=${ARG_ENABLE_AIBRIDGE:-false} - -echo "=== Codex Launch Configuration ===" -printf "OpenAI API Key: %s\n" "$([ -n "$ARG_OPENAI_API_KEY" ] && echo "Provided" || echo "Not provided")" -printf "Codex Model: %s\n" "${ARG_CODEX_MODEL:-"Default"}" -printf "Start Directory: %s\n" "$ARG_CODEX_START_DIRECTORY" -printf "Has Task Prompt: %s\n" "$([ -n "$ARG_CODEX_TASK_PROMPT" ] && echo "Yes" || echo "No")" -printf "Report Tasks: %s\n" "$ARG_REPORT_TASKS" -printf "Continue Sessions: %s\n" "$ARG_CONTINUE" -printf "Enable Coder AI Bridge: %s\n" "$ARG_ENABLE_AIBRIDGE" -echo "======================================" -set +o nounset - -SESSION_TRACKING_FILE="$HOME/.codex-module/.codex-task-session" - -find_session_for_directory() { - local target_dir="$1" - - if [ ! -f "$SESSION_TRACKING_FILE" ]; then - return 1 - fi - - local session_id - session_id=$(grep "^$target_dir|" "$SESSION_TRACKING_FILE" | cut -d'|' -f2 | head -1) - - if [ -n "$session_id" ]; then - echo "$session_id" - return 0 - fi - - return 1 -} - -store_session_mapping() { - local dir="$1" - local session_id="$2" - - mkdir -p "$(dirname "$SESSION_TRACKING_FILE")" - - if [ -f "$SESSION_TRACKING_FILE" ]; then - grep -v "^$dir|" "$SESSION_TRACKING_FILE" > "$SESSION_TRACKING_FILE.tmp" 2> /dev/null || true - mv "$SESSION_TRACKING_FILE.tmp" "$SESSION_TRACKING_FILE" - fi - - echo "$dir|$session_id" >> "$SESSION_TRACKING_FILE" -} - -find_recent_session_file() { - local target_dir="$1" - local sessions_dir="$HOME/.codex/sessions" - - if [ ! -d "$sessions_dir" ]; then - return 1 - fi - - local latest_file="" - local latest_time=0 - - while IFS= read -r session_file; do - local file_time - file_time=$(stat -c %Y "$session_file" 2> /dev/null || stat -f %m "$session_file" 2> /dev/null || echo "0") - local first_line - first_line=$(head -n 1 "$session_file" 2> /dev/null) - local session_cwd - session_cwd=$(echo "$first_line" | grep -o '"cwd":"[^"]*"' | cut -d'"' -f4) - - if [ "$session_cwd" = "$target_dir" ] && [ "$file_time" -gt "$latest_time" ]; then - latest_file="$session_file" - latest_time="$file_time" - fi - done < <(find "$sessions_dir" -type f -name "*.jsonl" 2> /dev/null) - - if [ -n "$latest_file" ]; then - local first_line - first_line=$(head -n 1 "$latest_file") - local session_id - session_id=$(echo "$first_line" | grep -o '"id":"[^"]*"' | cut -d'"' -f4) - if [ -n "$session_id" ]; then - echo "$session_id" - return 0 - fi - fi - - return 1 -} - -wait_for_session_file() { - local target_dir="$1" - local max_attempts=20 - local attempt=0 - - while [ $attempt -lt $max_attempts ]; do - local session_id - session_id=$(find_recent_session_file "$target_dir" 2> /dev/null || echo "") - if [ -n "$session_id" ]; then - echo "$session_id" - return 0 - fi - sleep 0.5 - attempt=$((attempt + 1)) - done - - return 1 -} - -validate_codex_installation() { - if command_exists codex; then - printf "Codex is installed\n" - else - printf "Error: Codex is not installed. Please enable install_codex or install it manually\n" - exit 1 - fi -} - -setup_workdir() { - if [ -d "${ARG_CODEX_START_DIRECTORY}" ]; then - printf "Directory '%s' exists. Changing to it.\\n" "${ARG_CODEX_START_DIRECTORY}" - cd "${ARG_CODEX_START_DIRECTORY}" || { - printf "Error: Could not change to directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}" - exit 1 - } - else - printf "Directory '%s' does not exist. Creating and changing to it.\\n" "${ARG_CODEX_START_DIRECTORY}" - mkdir -p "${ARG_CODEX_START_DIRECTORY}" || { - printf "Error: Could not create directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}" - exit 1 - } - cd "${ARG_CODEX_START_DIRECTORY}" || { - printf "Error: Could not change to directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}" - exit 1 - } - fi -} - -build_codex_args() { - CODEX_ARGS=() - - if [[ -n "${ARG_CODEX_MODEL}" ]]; then - CODEX_ARGS+=("--model" "${ARG_CODEX_MODEL}") - fi - - if [ "$ARG_CONTINUE" = "true" ]; then - existing_session=$(find_session_for_directory "$ARG_CODEX_START_DIRECTORY" 2> /dev/null || echo "") - - if [ -n "$existing_session" ]; then - printf "Found existing task session for this directory: %s\n" "$existing_session" - printf "Resuming existing session...\n" - CODEX_ARGS+=("resume" "$existing_session") - else - printf "No existing task session found for this directory\n" - printf "Starting new task session...\n" - - if [ -n "$ARG_CODEX_TASK_PROMPT" ]; then - if [ "${ARG_REPORT_TASKS}" == "true" ]; then - PROMPT="Complete the task at hand in one go. Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_CODEX_TASK_PROMPT" - else - PROMPT="Your task at hand: $ARG_CODEX_TASK_PROMPT" - fi - CODEX_ARGS+=("$PROMPT") - fi - fi - else - printf "Continue disabled, starting fresh session\n" - - if [ -n "$ARG_CODEX_TASK_PROMPT" ]; then - if [ "${ARG_REPORT_TASKS}" == "true" ]; then - PROMPT="Complete the task at hand in one go. Every step of the way, report your progress using Coder.coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_CODEX_TASK_PROMPT" - else - PROMPT="Your task at hand: $ARG_CODEX_TASK_PROMPT" - fi - CODEX_ARGS+=("$PROMPT") - fi - fi -} - -capture_session_id() { - if [ "$ARG_CONTINUE" = "true" ] && [ -z "$existing_session" ]; then - printf "Capturing new session ID...\n" - new_session=$(wait_for_session_file "$ARG_CODEX_START_DIRECTORY" || echo "") - - if [ -n "$new_session" ]; then - store_session_mapping "$ARG_CODEX_START_DIRECTORY" "$new_session" - printf "✓ Session tracked: %s\n" "$new_session" - printf "This session will be automatically resumed on next restart\n" - else - printf "⚠ Could not capture session ID after 10s timeout\n" - fi - fi -} - -start_codex() { - printf "Starting Codex with arguments: %s\n" "${CODEX_ARGS[*]}" - # AGENTAPI_BOUNDARY_PREFIX is set by the agentapi module's main.sh when - # enable_boundary=true. It points to a wrapper script that runs the command - # through coder boundary, sandboxing only the agent process. - if [ -n "${AGENTAPI_BOUNDARY_PREFIX:-}" ]; then - printf "Starting with coder boundary enabled\n" - agentapi server --type codex --term-width 67 --term-height 1190 -- \ - "${AGENTAPI_BOUNDARY_PREFIX}" codex "${CODEX_ARGS[@]}" & - else - agentapi server --type codex --term-width 67 --term-height 1190 -- codex "${CODEX_ARGS[@]}" & - fi - capture_session_id -} - -validate_codex_installation -setup_workdir -build_codex_args -start_codex diff --git a/registry/coder-labs/modules/codex/testdata/codex-mock.sh b/registry/coder-labs/modules/codex/testdata/codex-mock.sh index fe8f3806c..a73b70b5c 100644 --- a/registry/coder-labs/modules/codex/testdata/codex-mock.sh +++ b/registry/coder-labs/modules/codex/testdata/codex-mock.sh @@ -1,38 +1,9 @@ #!/bin/bash -# Handle --version flag if [[ "$1" == "--version" ]]; then - echo "HELLO: $(bash -c env)" echo "codex version v1.0.0" exit 0 fi -set -e - -SESSION_ID="" -IS_RESUME=false - -while [[ $# -gt 0 ]]; do - case $1 in - resume) - IS_RESUME=true - SESSION_ID="$2" - shift 2 - ;; - *) - shift - ;; - esac -done - -if [ "$IS_RESUME" = false ]; then - SESSION_ID="019a1234-5678-9abc-def0-123456789012" - echo "Created new session: $SESSION_ID" -else - echo "Resuming session: $SESSION_ID" -fi - -while true; do - echo "$(date) - codex-mock (session: $SESSION_ID)" - sleep 15 -done +echo "codex invoked with: $*" +exit 0 From 7e3493b4e8149a2682618fa559defe784a8edf5a Mon Sep 17 00:00:00 2001 From: Jay Kumar Date: Wed, 29 Apr 2026 02:55:42 +0000 Subject: [PATCH 02/25] fix(registry/coder-labs/modules/codex): remove sandbox_mode, approval_policy, and sandbox_workspace_write from default config --- registry/coder-labs/modules/codex/README.md | 16 +--------------- registry/coder-labs/modules/codex/main.test.ts | 6 ++---- registry/coder-labs/modules/codex/main.tf | 2 +- .../modules/codex/scripts/install.sh.tftpl | 5 ----- 4 files changed, 4 insertions(+), 25 deletions(-) diff --git a/registry/coder-labs/modules/codex/README.md b/registry/coder-labs/modules/codex/README.md index 0ee45e186..d80a7f86d 100644 --- a/registry/coder-labs/modules/codex/README.md +++ b/registry/coder-labs/modules/codex/README.md @@ -139,21 +139,7 @@ resource "coder_script" "post_codex" { ## Configuration -### Default Configuration - -When no custom `base_config_toml` is provided, the module uses these defaults: - -```toml -sandbox_mode = "workspace-write" -approval_policy = "never" -preferred_auth_method = "apikey" - -[sandbox_workspace_write] -network_access = true -``` - -> [!NOTE] -> For containerized workspaces (Docker/Kubernetes), you may need `sandbox_mode = "danger-full-access"` to avoid permission issues. For advanced options, see [Codex config docs](https://github.com/openai/codex/blob/main/codex-rs/config.md). +When no custom `base_config_toml` is provided, the module uses a minimal default with `preferred_auth_method = "apikey"`. For advanced options, see [Codex config docs](https://github.com/openai/codex/blob/main/codex-rs/config.md). ## Troubleshooting diff --git a/registry/coder-labs/modules/codex/main.test.ts b/registry/coder-labs/modules/codex/main.test.ts index a358e352a..72bf40089 100644 --- a/registry/coder-labs/modules/codex/main.test.ts +++ b/registry/coder-labs/modules/codex/main.test.ts @@ -256,10 +256,8 @@ describe("codex", async () => { const { id, scripts } = await setup(); await runScripts(id, scripts); const resp = await readFileContainer(id, "/home/coder/.codex/config.toml"); - expect(resp).toContain('sandbox_mode = "workspace-write"'); - expect(resp).toContain('approval_policy = "never"'); - expect(resp).toContain("[sandbox_workspace_write]"); - expect(resp).toContain("network_access = true"); + expect(resp).toContain('preferred_auth_method = "apikey"'); + expect(resp).toContain("[notice.model_migrations]"); }); test("pre-post-install-scripts", async () => { diff --git a/registry/coder-labs/modules/codex/main.tf b/registry/coder-labs/modules/codex/main.tf index 364b4e4c8..5298a8427 100644 --- a/registry/coder-labs/modules/codex/main.tf +++ b/registry/coder-labs/modules/codex/main.tf @@ -68,7 +68,7 @@ variable "codex_model" { variable "base_config_toml" { type = string - description = "Complete base TOML configuration for Codex (without mcp_servers section). If empty, uses minimal default configuration with workspace-write sandbox mode and never approval policy." + description = "Complete base TOML configuration for Codex (without mcp_servers section). If empty, uses minimal default configuration." default = "" } diff --git a/registry/coder-labs/modules/codex/scripts/install.sh.tftpl b/registry/coder-labs/modules/codex/scripts/install.sh.tftpl index b5ab0ef9e..929ecd260 100644 --- a/registry/coder-labs/modules/codex/scripts/install.sh.tftpl +++ b/registry/coder-labs/modules/codex/scripts/install.sh.tftpl @@ -95,14 +95,9 @@ function write_minimal_default_config() { fi cat << EOF > "$${config_path}" -sandbox_mode = "workspace-write" -approval_policy = "never" preferred_auth_method = "apikey" $${optional_config} -[sandbox_workspace_write] -network_access = true - [notice.model_migrations] "$${ARG_CODEX_MODEL}" = "$${ARG_LATEST_CODEX_MODEL}" From d33796e84908bdb5ed1be367310b18e014a71f47 Mon Sep 17 00:00:00 2001 From: Jay Kumar Date: Wed, 29 Apr 2026 03:02:59 +0000 Subject: [PATCH 03/25] docs(registry/coder-labs/modules/codex): describe base_config_toml default behavior with heredoc --- registry/coder-labs/modules/codex/main.tf | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/registry/coder-labs/modules/codex/main.tf b/registry/coder-labs/modules/codex/main.tf index 5298a8427..6d70ba202 100644 --- a/registry/coder-labs/modules/codex/main.tf +++ b/registry/coder-labs/modules/codex/main.tf @@ -68,7 +68,23 @@ variable "codex_model" { variable "base_config_toml" { type = string - description = "Complete base TOML configuration for Codex (without mcp_servers section). If empty, uses minimal default configuration." + description = <<-EOT + Complete base TOML configuration for Codex (without mcp_servers section). + When empty, the module generates a minimal default: + + preferred_auth_method = "apikey" + # model_provider = "aibridge" (when enable_ai_gateway = true) + # model_reasoning_effort = "" (when model_reasoning_effort is set) + + [notice.model_migrations] + "" = "" + + [projects.""] (when workdir is set) + trust_level = "trusted" + + When non-empty, the value is written verbatim as the base of config.toml; + additional_mcp_servers and AI Gateway sections are still appended after it. + EOT default = "" } From b478e886163ec3f155d1815ef98d8b2d8a79d702 Mon Sep 17 00:00:00 2001 From: Jay Kumar Date: Wed, 29 Apr 2026 07:54:17 +0000 Subject: [PATCH 04/25] fix(registry/coder-labs/modules/codex): remove notice.model_migrations from default config --- registry/coder-labs/modules/codex/main.test.ts | 1 - registry/coder-labs/modules/codex/main.tf | 10 ++-------- .../coder-labs/modules/codex/scripts/install.sh.tftpl | 6 ------ 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/registry/coder-labs/modules/codex/main.test.ts b/registry/coder-labs/modules/codex/main.test.ts index 72bf40089..f26f76e67 100644 --- a/registry/coder-labs/modules/codex/main.test.ts +++ b/registry/coder-labs/modules/codex/main.test.ts @@ -257,7 +257,6 @@ describe("codex", async () => { await runScripts(id, scripts); const resp = await readFileContainer(id, "/home/coder/.codex/config.toml"); expect(resp).toContain('preferred_auth_method = "apikey"'); - expect(resp).toContain("[notice.model_migrations]"); }); test("pre-post-install-scripts", async () => { diff --git a/registry/coder-labs/modules/codex/main.tf b/registry/coder-labs/modules/codex/main.tf index 6d70ba202..5d11300c7 100644 --- a/registry/coder-labs/modules/codex/main.tf +++ b/registry/coder-labs/modules/codex/main.tf @@ -76,9 +76,6 @@ variable "base_config_toml" { # model_provider = "aibridge" (when enable_ai_gateway = true) # model_reasoning_effort = "" (when model_reasoning_effort is set) - [notice.model_migrations] - "" = "" - [projects.""] (when workdir is set) trust_level = "trusted" @@ -132,9 +129,8 @@ resource "coder_env" "ai_gateway_session_token" { } locals { - workdir = var.workdir != null ? trimsuffix(var.workdir, "/") : "" - latest_codex_model = "gpt-5.4" - aibridge_config = <<-EOF + workdir = var.workdir != null ? trimsuffix(var.workdir, "/") : "" + aibridge_config = <<-EOF [model_providers.aibridge] name = "AI Bridge" base_url = "${data.coder_workspace.me.access_url}/api/v2/aibridge/openai/v1" @@ -146,8 +142,6 @@ locals { ARG_INSTALL = tostring(var.install_codex) ARG_CODEX_VERSION = var.codex_version ARG_WORKDIR = local.workdir - ARG_CODEX_MODEL = var.codex_model - ARG_LATEST_CODEX_MODEL = local.latest_codex_model ARG_BASE_CONFIG_TOML = var.base_config_toml != "" ? base64encode(var.base_config_toml) : "" ARG_ADDITIONAL_MCP_SERVERS = var.additional_mcp_servers != "" ? base64encode(var.additional_mcp_servers) : "" ARG_ENABLE_AI_GATEWAY = tostring(var.enable_ai_gateway) diff --git a/registry/coder-labs/modules/codex/scripts/install.sh.tftpl b/registry/coder-labs/modules/codex/scripts/install.sh.tftpl index 929ecd260..7a4d0842f 100644 --- a/registry/coder-labs/modules/codex/scripts/install.sh.tftpl +++ b/registry/coder-labs/modules/codex/scripts/install.sh.tftpl @@ -11,8 +11,6 @@ command_exists() { ARG_INSTALL='${ARG_INSTALL}' ARG_CODEX_VERSION='${ARG_CODEX_VERSION}' ARG_WORKDIR='${ARG_WORKDIR}' -ARG_CODEX_MODEL='${ARG_CODEX_MODEL}' -ARG_LATEST_CODEX_MODEL='${ARG_LATEST_CODEX_MODEL}' ARG_BASE_CONFIG_TOML=$(echo -n '${ARG_BASE_CONFIG_TOML}' | base64 -d) ARG_ADDITIONAL_MCP_SERVERS=$(echo -n '${ARG_ADDITIONAL_MCP_SERVERS}' | base64 -d) ARG_ENABLE_AI_GATEWAY='${ARG_ENABLE_AI_GATEWAY}' @@ -23,7 +21,6 @@ echo "--------------------------------" printf "ARG_INSTALL: %s\n" "$${ARG_INSTALL}" printf "ARG_CODEX_VERSION: %s\n" "$${ARG_CODEX_VERSION}" printf "ARG_WORKDIR: %s\n" "$${ARG_WORKDIR}" -printf "ARG_CODEX_MODEL: %s\n" "$${ARG_CODEX_MODEL}" printf "ARG_ENABLE_AI_GATEWAY: %s\n" "$${ARG_ENABLE_AI_GATEWAY}" echo "--------------------------------" @@ -98,9 +95,6 @@ function write_minimal_default_config() { preferred_auth_method = "apikey" $${optional_config} -[notice.model_migrations] -"$${ARG_CODEX_MODEL}" = "$${ARG_LATEST_CODEX_MODEL}" - EOF if [ -n "$${ARG_WORKDIR}" ]; then From e98831dc219aadbbed7673e0dbabbdb01eed1971 Mon Sep 17 00:00:00 2001 From: Jay Kumar Date: Wed, 29 Apr 2026 07:57:00 +0000 Subject: [PATCH 05/25] docs(registry/coder-labs/modules/codex): clarify model_provider and model_reasoning_effort descriptions --- registry/coder-labs/modules/codex/main.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/registry/coder-labs/modules/codex/main.tf b/registry/coder-labs/modules/codex/main.tf index 5d11300c7..f4f0fd981 100644 --- a/registry/coder-labs/modules/codex/main.tf +++ b/registry/coder-labs/modules/codex/main.tf @@ -73,8 +73,8 @@ variable "base_config_toml" { When empty, the module generates a minimal default: preferred_auth_method = "apikey" - # model_provider = "aibridge" (when enable_ai_gateway = true) - # model_reasoning_effort = "" (when model_reasoning_effort is set) + # model_provider = "aibridge" (sets the default profile, when enable_ai_gateway = true) + # model_reasoning_effort = "" (sets the reasoning effort, when model_reasoning_effort is set) [projects.""] (when workdir is set) trust_level = "trusted" From 63c2d97cf75a7d66cf8fef78461614ec431638cc Mon Sep 17 00:00:00 2001 From: Jay Kumar Date: Wed, 29 Apr 2026 08:35:39 +0000 Subject: [PATCH 06/25] refactor(registry/coder-labs/modules/codex): remove dead codex_model var, add test coverage - Remove codex_model variable (unused after model_migrations removal) - Add model_reasoning_effort assertion to AI gateway test - Add workdir-trusted-project and no-workdir-no-project-section tests - Run bun fmt --- registry/coder-labs/modules/codex/README.md | 1 - .../coder-labs/modules/codex/main.test.ts | 36 ++++++++++++++++++- registry/coder-labs/modules/codex/main.tf | 6 ---- .../coder-labs/modules/codex/main.tftest.hcl | 6 ---- 4 files changed, 35 insertions(+), 14 deletions(-) diff --git a/registry/coder-labs/modules/codex/README.md b/registry/coder-labs/modules/codex/README.md index d80a7f86d..05a7d3d2e 100644 --- a/registry/coder-labs/modules/codex/README.md +++ b/registry/coder-labs/modules/codex/README.md @@ -92,7 +92,6 @@ module "codex" { openai_api_key = var.openai_api_key codex_version = "0.1.0" - codex_model = "gpt-4o" base_config_toml = <<-EOT sandbox_mode = "danger-full-access" diff --git a/registry/coder-labs/modules/codex/main.test.ts b/registry/coder-labs/modules/codex/main.test.ts index f26f76e67..a9c2d76af 100644 --- a/registry/coder-labs/modules/codex/main.test.ts +++ b/registry/coder-labs/modules/codex/main.test.ts @@ -15,7 +15,10 @@ import { runTerraformInit, TerraformState, } from "~test"; -import { extractCoderEnvVars, writeExecutable } from "../../../coder/modules/agentapi/test-util"; +import { + extractCoderEnvVars, + writeExecutable, +} from "../../../coder/modules/agentapi/test-util"; import path from "path"; interface ModuleScripts { @@ -309,6 +312,37 @@ describe("codex", async () => { "/home/coder/.codex/config.toml", ); expect(configToml).toContain('model_provider = "aibridge"'); + expect(configToml).toContain('model_reasoning_effort = "none"'); expect(configToml).toContain("[model_providers.aibridge]"); }); + + test("workdir-trusted-project", async () => { + const workdir = "/home/coder/trusted-project"; + const { id, scripts } = await setup({ + moduleVariables: { + workdir, + }, + }); + await runScripts(id, scripts); + const configToml = await readFileContainer( + id, + "/home/coder/.codex/config.toml", + ); + expect(configToml).toContain(`[projects."${workdir}"]`); + expect(configToml).toContain('trust_level = "trusted"'); + }); + + test("no-workdir-no-project-section", async () => { + const { id, scripts } = await setup({ + moduleVariables: { + workdir: "", + }, + }); + await runScripts(id, scripts); + const configToml = await readFileContainer( + id, + "/home/coder/.codex/config.toml", + ); + expect(configToml).not.toContain("[projects."); + }); }); diff --git a/registry/coder-labs/modules/codex/main.tf b/registry/coder-labs/modules/codex/main.tf index f4f0fd981..3cbbc2741 100644 --- a/registry/coder-labs/modules/codex/main.tf +++ b/registry/coder-labs/modules/codex/main.tf @@ -60,12 +60,6 @@ variable "openai_api_key" { default = "" } -variable "codex_model" { - type = string - description = "The model for Codex to use." - default = "gpt-5.4" -} - variable "base_config_toml" { type = string description = <<-EOT diff --git a/registry/coder-labs/modules/codex/main.tftest.hcl b/registry/coder-labs/modules/codex/main.tftest.hcl index b41cd6717..986823365 100644 --- a/registry/coder-labs/modules/codex/main.tftest.hcl +++ b/registry/coder-labs/modules/codex/main.tftest.hcl @@ -39,7 +39,6 @@ run "test_codex_custom_options" { agent_id = "test-agent" workdir = "/home/coder/project" icon = "/icon/custom.svg" - codex_model = "gpt-4o" codex_version = "0.1.0" } @@ -47,11 +46,6 @@ run "test_codex_custom_options" { condition = var.icon == "/icon/custom.svg" error_message = "Icon should be set to custom icon" } - - assert { - condition = var.codex_model == "gpt-4o" - error_message = "codex_model should be set to 'gpt-4o'" - } } run "test_ai_gateway_enabled" { From 0539729955229ed57bd5257a78ea5d1b0c0f55cf Mon Sep 17 00:00:00 2001 From: Jay Kumar Date: Wed, 29 Apr 2026 09:49:11 +0000 Subject: [PATCH 07/25] refactor(registry/coder-labs/modules/codex): remove install_codex, node installation, and ARG_INSTALL Codex always installs via npm. Removed the install_codex toggle, the install_node/nvm bootstrap, and the ARG_INSTALL plumbing. --- .../coder-labs/modules/codex/main.test.ts | 21 +-------- registry/coder-labs/modules/codex/main.tf | 7 --- .../coder-labs/modules/codex/main.tftest.hcl | 4 -- .../modules/codex/scripts/install.sh.tftpl | 46 ------------------- 4 files changed, 2 insertions(+), 76 deletions(-) diff --git a/registry/coder-labs/modules/codex/main.test.ts b/registry/coder-labs/modules/codex/main.test.ts index a9c2d76af..c22502205 100644 --- a/registry/coder-labs/modules/codex/main.test.ts +++ b/registry/coder-labs/modules/codex/main.test.ts @@ -101,7 +101,6 @@ const setup = async ( const state = await runTerraformApply(moduleDir, { agent_id: "foo", workdir: projectDir, - install_codex: "false", ...props?.moduleVariables, }); const scripts = collectScripts(state); @@ -185,24 +184,7 @@ describe("codex", async () => { id, "/home/coder/.coder-modules/coder-labs/codex/logs/install.log", ); - expect(installLog).toContain("Skipping Codex installation"); - }); - - test("install-codex-version", async () => { - const version = "0.10.0"; - const { id, coderEnvVars, scripts } = await setup({ - skipCodexMock: true, - moduleVariables: { - install_codex: "true", - codex_version: version, - }, - }); - await runScripts(id, scripts, coderEnvVars); - const installLog = await readFileContainer( - id, - "/home/coder/.coder-modules/coder-labs/codex/logs/install.log", - ); - expect(installLog).toContain(version); + expect(installLog).toContain("Installed Codex CLI"); }); test("openai-api-key", async () => { @@ -301,6 +283,7 @@ describe("codex", async () => { test("codex-with-ai-gateway", async () => { const { id, coderEnvVars, scripts } = await setup({ + skipCodexMock: true, moduleVariables: { enable_ai_gateway: "true", model_reasoning_effort: "none", diff --git a/registry/coder-labs/modules/codex/main.tf b/registry/coder-labs/modules/codex/main.tf index 3cbbc2741..4346d05cb 100644 --- a/registry/coder-labs/modules/codex/main.tf +++ b/registry/coder-labs/modules/codex/main.tf @@ -42,12 +42,6 @@ variable "post_install_script" { default = null } -variable "install_codex" { - type = bool - description = "Whether to install Codex." - default = true -} - variable "codex_version" { type = string description = "The version of Codex to install." @@ -133,7 +127,6 @@ locals { EOF install_script = templatefile("${path.module}/scripts/install.sh.tftpl", { - ARG_INSTALL = tostring(var.install_codex) ARG_CODEX_VERSION = var.codex_version ARG_WORKDIR = local.workdir ARG_BASE_CONFIG_TOML = var.base_config_toml != "" ? base64encode(var.base_config_toml) : "" diff --git a/registry/coder-labs/modules/codex/main.tftest.hcl b/registry/coder-labs/modules/codex/main.tftest.hcl index 986823365..3c7ec3bf3 100644 --- a/registry/coder-labs/modules/codex/main.tftest.hcl +++ b/registry/coder-labs/modules/codex/main.tftest.hcl @@ -11,10 +11,6 @@ run "test_codex_basic" { error_message = "Workdir should be set correctly" } - assert { - condition = var.install_codex == true - error_message = "install_codex should default to true" - } } run "test_codex_with_api_key" { diff --git a/registry/coder-labs/modules/codex/scripts/install.sh.tftpl b/registry/coder-labs/modules/codex/scripts/install.sh.tftpl index 7a4d0842f..16be2fb61 100644 --- a/registry/coder-labs/modules/codex/scripts/install.sh.tftpl +++ b/registry/coder-labs/modules/codex/scripts/install.sh.tftpl @@ -8,7 +8,6 @@ command_exists() { command -v "$1" > /dev/null 2>&1 } -ARG_INSTALL='${ARG_INSTALL}' ARG_CODEX_VERSION='${ARG_CODEX_VERSION}' ARG_WORKDIR='${ARG_WORKDIR}' ARG_BASE_CONFIG_TOML=$(echo -n '${ARG_BASE_CONFIG_TOML}' | base64 -d) @@ -18,57 +17,12 @@ ARG_AIBRIDGE_CONFIG=$(echo -n '${ARG_AIBRIDGE_CONFIG}' | base64 -d) ARG_MODEL_REASONING_EFFORT='${ARG_MODEL_REASONING_EFFORT}' echo "--------------------------------" -printf "ARG_INSTALL: %s\n" "$${ARG_INSTALL}" printf "ARG_CODEX_VERSION: %s\n" "$${ARG_CODEX_VERSION}" printf "ARG_WORKDIR: %s\n" "$${ARG_WORKDIR}" printf "ARG_ENABLE_AI_GATEWAY: %s\n" "$${ARG_ENABLE_AI_GATEWAY}" echo "--------------------------------" -function install_node() { - if ! command_exists npm; then - printf "npm not found, checking for Node.js installation...\n" - if ! command_exists node; then - printf "Node.js not found, installing Node.js via NVM...\n" - export NVM_DIR="$HOME/.nvm" - if [ ! -d "$NVM_DIR" ]; then - mkdir -p "$NVM_DIR" - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash - [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" - else - [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" - fi - - nvm install --lts - nvm use --lts - nvm alias default node - - printf "Node.js installed: %s\n" "$(node --version)" - printf "npm installed: %s\n" "$(npm --version)" - else - printf "Node.js is installed but npm is not available.\n" - exit 1 - fi - fi -} - function install_codex() { - if [ "$${ARG_INSTALL}" != "true" ]; then - echo "Skipping Codex installation as per configuration." - return - fi - - install_node - - if ! command_exists nvm; then - mkdir -p "$HOME"/.npm-global - npm config set prefix "$HOME/.npm-global" - export PATH="$HOME/.npm-global/bin:$PATH" - - if ! grep -q "export PATH=$HOME/.npm-global/bin:\$PATH" ~/.bashrc 2>/dev/null; then - echo "export PATH=$HOME/.npm-global/bin:\$PATH" >> ~/.bashrc - fi - fi - printf "%s Installing Codex CLI\n" "$${BOLD}" if [ -n "$${ARG_CODEX_VERSION}" ]; then From 06378c240b1def915ffcbf6dc999e0dcbe433c9c Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Wed, 29 Apr 2026 16:15:09 +0530 Subject: [PATCH 08/25] debug --- registry/coder-labs/modules/codex/scripts/install.sh.tftpl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/registry/coder-labs/modules/codex/scripts/install.sh.tftpl b/registry/coder-labs/modules/codex/scripts/install.sh.tftpl index 16be2fb61..154e469fe 100644 --- a/registry/coder-labs/modules/codex/scripts/install.sh.tftpl +++ b/registry/coder-labs/modules/codex/scripts/install.sh.tftpl @@ -1,5 +1,9 @@ #!/bin/bash +if [ -f "$HOME/.bashrc" ]; then + source "$HOME"/.bashrc +fi + set -euo pipefail BOLD='\033[0;1m' From bb9633d8fe36c3b78e9ac99665c21078a2bd36ec Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Wed, 29 Apr 2026 17:17:28 +0530 Subject: [PATCH 09/25] debug --- registry/coder-labs/modules/codex/scripts/install.sh.tftpl | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/registry/coder-labs/modules/codex/scripts/install.sh.tftpl b/registry/coder-labs/modules/codex/scripts/install.sh.tftpl index 154e469fe..2f9dd39aa 100644 --- a/registry/coder-labs/modules/codex/scripts/install.sh.tftpl +++ b/registry/coder-labs/modules/codex/scripts/install.sh.tftpl @@ -1,13 +1,12 @@ #!/bin/bash -if [ -f "$HOME/.bashrc" ]; then - source "$HOME"/.bashrc -fi - set -euo pipefail BOLD='\033[0;1m' +export NVM_DIR="$HOME/.nvm" +[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" + command_exists() { command -v "$1" > /dev/null 2>&1 } From 91f80c299218e25d456f93dce5bc9262efc18769 Mon Sep 17 00:00:00 2001 From: Jay Kumar Date: Wed, 29 Apr 2026 11:57:37 +0000 Subject: [PATCH 10/25] fix(registry/coder-labs/modules/codex): restore install_codex variable and skip guard --- .../coder-labs/modules/codex/main.test.ts | 20 ++++++++++++++++++- registry/coder-labs/modules/codex/main.tf | 7 +++++++ .../coder-labs/modules/codex/main.tftest.hcl | 4 ++++ .../modules/codex/scripts/install.sh.tftpl | 6 ++++++ 4 files changed, 36 insertions(+), 1 deletion(-) diff --git a/registry/coder-labs/modules/codex/main.test.ts b/registry/coder-labs/modules/codex/main.test.ts index c22502205..9fb989732 100644 --- a/registry/coder-labs/modules/codex/main.test.ts +++ b/registry/coder-labs/modules/codex/main.test.ts @@ -101,6 +101,7 @@ const setup = async ( const state = await runTerraformApply(moduleDir, { agent_id: "foo", workdir: projectDir, + install_codex: "false", ...props?.moduleVariables, }); const scripts = collectScripts(state); @@ -184,7 +185,24 @@ describe("codex", async () => { id, "/home/coder/.coder-modules/coder-labs/codex/logs/install.log", ); - expect(installLog).toContain("Installed Codex CLI"); + expect(installLog).toContain("Skipping Codex installation"); + }); + + test("install-codex-version", async () => { + const version = "0.10.0"; + const { id, coderEnvVars, scripts } = await setup({ + skipCodexMock: true, + moduleVariables: { + install_codex: "true", + codex_version: version, + }, + }); + await runScripts(id, scripts, coderEnvVars); + const installLog = await readFileContainer( + id, + "/home/coder/.coder-modules/coder-labs/codex/logs/install.log", + ); + expect(installLog).toContain(version); }); test("openai-api-key", async () => { diff --git a/registry/coder-labs/modules/codex/main.tf b/registry/coder-labs/modules/codex/main.tf index 4346d05cb..3cbbc2741 100644 --- a/registry/coder-labs/modules/codex/main.tf +++ b/registry/coder-labs/modules/codex/main.tf @@ -42,6 +42,12 @@ variable "post_install_script" { default = null } +variable "install_codex" { + type = bool + description = "Whether to install Codex." + default = true +} + variable "codex_version" { type = string description = "The version of Codex to install." @@ -127,6 +133,7 @@ locals { EOF install_script = templatefile("${path.module}/scripts/install.sh.tftpl", { + ARG_INSTALL = tostring(var.install_codex) ARG_CODEX_VERSION = var.codex_version ARG_WORKDIR = local.workdir ARG_BASE_CONFIG_TOML = var.base_config_toml != "" ? base64encode(var.base_config_toml) : "" diff --git a/registry/coder-labs/modules/codex/main.tftest.hcl b/registry/coder-labs/modules/codex/main.tftest.hcl index 3c7ec3bf3..986823365 100644 --- a/registry/coder-labs/modules/codex/main.tftest.hcl +++ b/registry/coder-labs/modules/codex/main.tftest.hcl @@ -11,6 +11,10 @@ run "test_codex_basic" { error_message = "Workdir should be set correctly" } + assert { + condition = var.install_codex == true + error_message = "install_codex should default to true" + } } run "test_codex_with_api_key" { diff --git a/registry/coder-labs/modules/codex/scripts/install.sh.tftpl b/registry/coder-labs/modules/codex/scripts/install.sh.tftpl index 2f9dd39aa..8238d91a9 100644 --- a/registry/coder-labs/modules/codex/scripts/install.sh.tftpl +++ b/registry/coder-labs/modules/codex/scripts/install.sh.tftpl @@ -11,6 +11,7 @@ command_exists() { command -v "$1" > /dev/null 2>&1 } +ARG_INSTALL='${ARG_INSTALL}' ARG_CODEX_VERSION='${ARG_CODEX_VERSION}' ARG_WORKDIR='${ARG_WORKDIR}' ARG_BASE_CONFIG_TOML=$(echo -n '${ARG_BASE_CONFIG_TOML}' | base64 -d) @@ -26,6 +27,11 @@ printf "ARG_ENABLE_AI_GATEWAY: %s\n" "$${ARG_ENABLE_AI_GATEWAY}" echo "--------------------------------" function install_codex() { + if [ "$${ARG_INSTALL}" != "true" ]; then + echo "Skipping Codex installation as per configuration." + return + fi + printf "%s Installing Codex CLI\n" "$${BOLD}" if [ -n "$${ARG_CODEX_VERSION}" ]; then From f33a28288293880f07fe63d8b22794313353e0aa Mon Sep 17 00:00:00 2001 From: Jay Kumar Date: Wed, 29 Apr 2026 12:04:43 +0000 Subject: [PATCH 11/25] fix(registry/coder-labs/modules/codex): move NVM source inside install_codex guard --- .../coder-labs/modules/codex/scripts/install.sh.tftpl | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/registry/coder-labs/modules/codex/scripts/install.sh.tftpl b/registry/coder-labs/modules/codex/scripts/install.sh.tftpl index 8238d91a9..473d1c8af 100644 --- a/registry/coder-labs/modules/codex/scripts/install.sh.tftpl +++ b/registry/coder-labs/modules/codex/scripts/install.sh.tftpl @@ -4,9 +4,6 @@ set -euo pipefail BOLD='\033[0;1m' -export NVM_DIR="$HOME/.nvm" -[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" - command_exists() { command -v "$1" > /dev/null 2>&1 } @@ -32,6 +29,11 @@ function install_codex() { return fi + if [ -s "$HOME/.nvm/nvm.sh" ]; then + export NVM_DIR="$HOME/.nvm" + . "$NVM_DIR/nvm.sh" + fi + printf "%s Installing Codex CLI\n" "$${BOLD}" if [ -n "$${ARG_CODEX_VERSION}" ]; then From e835ccef4486fe63bf38608294f30ac373d1e96a Mon Sep 17 00:00:00 2001 From: Jay Kumar Date: Wed, 29 Apr 2026 12:25:25 +0000 Subject: [PATCH 12/25] fix(registry/coder-labs/modules/codex): add npm-global prefix fallback for non-root installs When NVM is not available, set npm prefix to ~/.npm-global so npm install -g works without root permissions. --- registry/coder-labs/modules/codex/scripts/install.sh.tftpl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/registry/coder-labs/modules/codex/scripts/install.sh.tftpl b/registry/coder-labs/modules/codex/scripts/install.sh.tftpl index 473d1c8af..89eb5e7e2 100644 --- a/registry/coder-labs/modules/codex/scripts/install.sh.tftpl +++ b/registry/coder-labs/modules/codex/scripts/install.sh.tftpl @@ -32,6 +32,10 @@ function install_codex() { if [ -s "$HOME/.nvm/nvm.sh" ]; then export NVM_DIR="$HOME/.nvm" . "$NVM_DIR/nvm.sh" + elif ! command_exists nvm; then + mkdir -p "$HOME/.npm-global" + npm config set prefix "$HOME/.npm-global" + export PATH="$HOME/.npm-global/bin:$PATH" fi printf "%s Installing Codex CLI\n" "$${BOLD}" From 013dc7bd7fbfb1d4b27a1d330455d5dfb85101fc Mon Sep 17 00:00:00 2001 From: Jay Kumar Date: Wed, 29 Apr 2026 12:32:42 +0000 Subject: [PATCH 13/25] docs(registry/coder-labs/modules/codex): remove Prerequisites and workdir sections from README --- registry/coder-labs/modules/codex/README.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/registry/coder-labs/modules/codex/README.md b/registry/coder-labs/modules/codex/README.md index 05a7d3d2e..a84db4607 100644 --- a/registry/coder-labs/modules/codex/README.md +++ b/registry/coder-labs/modules/codex/README.md @@ -22,14 +22,6 @@ module "codex" { > [!WARNING] > If upgrading from v4.x.x of this module: v5 is a major refactor that drops support for [Coder Tasks](https://coder.com/docs/ai-coder/tasks) and [Boundary](https://coder.com/docs/ai-coder/agent-firewall). Keep using v4.x.x if you depend on them. -## Prerequisites - -- OpenAI API key, or Coder AI Gateway (`enable_ai_gateway = true`, requires Coder >= 2.30.0). - -## workdir - -`workdir` is optional. When set, the module pre-creates the directory if it is missing and adds it as a trusted project in `~/.codex/config.toml`. Leave `workdir` unset if you only want the module to install the CLI and configure authentication. - ## Examples ### Standalone mode with a launcher app From e53db1936ddeec3e8ad771b6fbe0187e29bf03a4 Mon Sep 17 00:00:00 2001 From: Jay Kumar Date: Wed, 29 Apr 2026 12:35:58 +0000 Subject: [PATCH 14/25] docs(registry/coder-labs/modules/codex): note that custom base_config_toml needs manual model_provider for AI Gateway --- registry/coder-labs/modules/codex/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/registry/coder-labs/modules/codex/README.md b/registry/coder-labs/modules/codex/README.md index a84db4607..2b3039c2d 100644 --- a/registry/coder-labs/modules/codex/README.md +++ b/registry/coder-labs/modules/codex/README.md @@ -73,6 +73,13 @@ When `enable_ai_gateway = true`, the module configures Codex to use the `aibridg > [!CAUTION] > `enable_ai_gateway = true` is mutually exclusive with `openai_api_key`. Setting both fails at plan time. +> [!NOTE] +> If you provide a custom `base_config_toml`, the module writes it verbatim and does not inject `model_provider = "aibridge"` automatically. Add it to your config yourself: +> +> ```toml +> model_provider = "aibridge" +> ``` + ### Advanced Configuration ```tf From a1c8bbf210a304e2b4c84d8dbcbe68c76e325526 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Wed, 29 Apr 2026 18:19:46 +0530 Subject: [PATCH 15/25] remove unwanted link --- registry/coder-labs/modules/codex/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/registry/coder-labs/modules/codex/README.md b/registry/coder-labs/modules/codex/README.md index 2b3039c2d..a48918df8 100644 --- a/registry/coder-labs/modules/codex/README.md +++ b/registry/coder-labs/modules/codex/README.md @@ -152,5 +152,4 @@ cat ~/.coder-modules/coder-labs/codex/logs/post_install.log ## References - [Codex CLI Documentation](https://github.com/openai/codex) -- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents) - [AI Gateway](https://coder.com/docs/ai-coder/ai-gateway) From 35844246844ea68c7fa8caafb4816a9d515023ab Mon Sep 17 00:00:00 2001 From: Jay Kumar Date: Thu, 30 Apr 2026 08:56:25 +0000 Subject: [PATCH 16/25] fix(registry/coder-labs/modules/codex): address review comments DEREM-6: mark openai_api_key as sensitive DEREM-7: base64-encode ARG_CODEX_VERSION and ARG_WORKDIR to prevent shell/TOML injection from user-controlled input DEREM-8: add test for AI gateway + custom base_config_toml, verifying provider section appended and no duplicates when user includes it DEREM-10: guard duplicate [model_providers.aibridge] with grep before appending DEREM-11: fail early with actionable error when npm is not available DEREM-13: add install-codex-latest test covering the unversioned path DEREM-16: mention npm requirement in v5 upgrade warning DEREM-17: restore allowed values and docs link on model_reasoning_effort, document empty-string behavior on codex_version DEREM-20: remove dead skipCodexMock from ai-gateway test DEREM-21: codex_version description now explains empty means latest --- registry/coder-labs/modules/codex/README.md | 3 +- .../coder-labs/modules/codex/main.test.ts | 62 ++++++++++++++++++- registry/coder-labs/modules/codex/main.tf | 9 +-- .../modules/codex/scripts/install.sh.tftpl | 23 ++++--- 4 files changed, 83 insertions(+), 14 deletions(-) diff --git a/registry/coder-labs/modules/codex/README.md b/registry/coder-labs/modules/codex/README.md index a48918df8..62f33c47b 100644 --- a/registry/coder-labs/modules/codex/README.md +++ b/registry/coder-labs/modules/codex/README.md @@ -20,7 +20,7 @@ module "codex" { ``` > [!WARNING] -> If upgrading from v4.x.x of this module: v5 is a major refactor that drops support for [Coder Tasks](https://coder.com/docs/ai-coder/tasks) and [Boundary](https://coder.com/docs/ai-coder/agent-firewall). Keep using v4.x.x if you depend on them. +> If upgrading from v4.x.x of this module: v5 is a major refactor that drops support for [Coder Tasks](https://coder.com/docs/ai-coder/tasks) and [Boundary](https://coder.com/docs/ai-coder/agent-firewall). v5 also assumes npm is pre-installed; it no longer bootstraps Node.js. Keep using v4.x.x if you depend on them. ## Examples @@ -152,4 +152,3 @@ cat ~/.coder-modules/coder-labs/codex/logs/post_install.log ## References - [Codex CLI Documentation](https://github.com/openai/codex) -- [AI Gateway](https://coder.com/docs/ai-coder/ai-gateway) diff --git a/registry/coder-labs/modules/codex/main.test.ts b/registry/coder-labs/modules/codex/main.test.ts index 9fb989732..750a9ecd7 100644 --- a/registry/coder-labs/modules/codex/main.test.ts +++ b/registry/coder-labs/modules/codex/main.test.ts @@ -301,7 +301,6 @@ describe("codex", async () => { test("codex-with-ai-gateway", async () => { const { id, coderEnvVars, scripts } = await setup({ - skipCodexMock: true, moduleVariables: { enable_ai_gateway: "true", model_reasoning_effort: "none", @@ -346,4 +345,65 @@ describe("codex", async () => { ); expect(configToml).not.toContain("[projects."); }); + + test("ai-gateway-with-custom-base-config", async () => { + const baseConfig = [ + 'sandbox_mode = "danger-full-access"', + 'model_provider = "aibridge"', + ].join("\n"); + const { id, coderEnvVars, scripts } = await setup({ + moduleVariables: { + enable_ai_gateway: "true", + base_config_toml: baseConfig, + }, + }); + await runScripts(id, scripts, coderEnvVars); + const configToml = await readFileContainer( + id, + "/home/coder/.codex/config.toml", + ); + expect(configToml).toContain('model_provider = "aibridge"'); + expect(configToml).toContain("[model_providers.aibridge]"); + }); + + test("ai-gateway-custom-config-no-duplicate-provider", async () => { + const baseConfig = [ + 'model_provider = "aibridge"', + "", + "[model_providers.aibridge]", + 'name = "Custom AI Bridge"', + 'base_url = "https://custom.example.com"', + 'env_key = "CODER_AIBRIDGE_SESSION_TOKEN"', + 'wire_api = "responses"', + ].join("\n"); + const { id, coderEnvVars, scripts } = await setup({ + moduleVariables: { + enable_ai_gateway: "true", + base_config_toml: baseConfig, + }, + }); + await runScripts(id, scripts, coderEnvVars); + const configToml = await readFileContainer( + id, + "/home/coder/.codex/config.toml", + ); + const matches = configToml.match(/\[model_providers\.aibridge\]/g) || []; + expect(matches.length).toBe(1); + expect(configToml).toContain("Custom AI Bridge"); + }); + + test("install-codex-latest", async () => { + const { id, coderEnvVars, scripts } = await setup({ + skipCodexMock: true, + moduleVariables: { + install_codex: "true", + }, + }); + await runScripts(id, scripts, coderEnvVars); + const installLog = await readFileContainer( + id, + "/home/coder/.coder-modules/coder-labs/codex/logs/install.log", + ); + expect(installLog).toContain("Installed Codex CLI"); + }); }); diff --git a/registry/coder-labs/modules/codex/main.tf b/registry/coder-labs/modules/codex/main.tf index 3cbbc2741..c4a0a3e3d 100644 --- a/registry/coder-labs/modules/codex/main.tf +++ b/registry/coder-labs/modules/codex/main.tf @@ -50,13 +50,14 @@ variable "install_codex" { variable "codex_version" { type = string - description = "The version of Codex to install." + description = "The version of Codex to install. Empty string installs the latest available version." default = "" } variable "openai_api_key" { type = string description = "OpenAI API key for Codex CLI." + sensitive = true default = "" } @@ -87,7 +88,7 @@ variable "additional_mcp_servers" { variable "model_reasoning_effort" { type = string - description = "The reasoning effort for the model." + 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" default = "" validation { condition = contains(["", "none", "minimal", "low", "medium", "high", "xhigh"], var.model_reasoning_effort) @@ -134,8 +135,8 @@ locals { EOF install_script = templatefile("${path.module}/scripts/install.sh.tftpl", { ARG_INSTALL = tostring(var.install_codex) - ARG_CODEX_VERSION = var.codex_version - ARG_WORKDIR = local.workdir + ARG_CODEX_VERSION = var.codex_version != "" ? base64encode(var.codex_version) : "" + ARG_WORKDIR = local.workdir != "" ? base64encode(local.workdir) : "" ARG_BASE_CONFIG_TOML = var.base_config_toml != "" ? base64encode(var.base_config_toml) : "" ARG_ADDITIONAL_MCP_SERVERS = var.additional_mcp_servers != "" ? base64encode(var.additional_mcp_servers) : "" ARG_ENABLE_AI_GATEWAY = tostring(var.enable_ai_gateway) diff --git a/registry/coder-labs/modules/codex/scripts/install.sh.tftpl b/registry/coder-labs/modules/codex/scripts/install.sh.tftpl index 89eb5e7e2..1b22793a2 100644 --- a/registry/coder-labs/modules/codex/scripts/install.sh.tftpl +++ b/registry/coder-labs/modules/codex/scripts/install.sh.tftpl @@ -9,8 +9,8 @@ command_exists() { } ARG_INSTALL='${ARG_INSTALL}' -ARG_CODEX_VERSION='${ARG_CODEX_VERSION}' -ARG_WORKDIR='${ARG_WORKDIR}' +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_ADDITIONAL_MCP_SERVERS=$(echo -n '${ARG_ADDITIONAL_MCP_SERVERS}' | base64 -d) ARG_ENABLE_AI_GATEWAY='${ARG_ENABLE_AI_GATEWAY}' @@ -18,9 +18,9 @@ ARG_AIBRIDGE_CONFIG=$(echo -n '${ARG_AIBRIDGE_CONFIG}' | base64 -d) ARG_MODEL_REASONING_EFFORT='${ARG_MODEL_REASONING_EFFORT}' echo "--------------------------------" -printf "ARG_CODEX_VERSION: %s\n" "$${ARG_CODEX_VERSION}" -printf "ARG_WORKDIR: %s\n" "$${ARG_WORKDIR}" -printf "ARG_ENABLE_AI_GATEWAY: %s\n" "$${ARG_ENABLE_AI_GATEWAY}" +printf "codex_version: %s\n" "$${ARG_CODEX_VERSION}" +printf "workdir: %s\n" "$${ARG_WORKDIR}" +printf "enable_ai_gateway: %s\n" "$${ARG_ENABLE_AI_GATEWAY}" echo "--------------------------------" function install_codex() { @@ -29,6 +29,11 @@ function install_codex() { return fi + if ! command_exists npm; then + echo "Error: npm is required to install Codex. Install Node.js/npm first or set install_codex = false." + exit 1 + fi + if [ -s "$HOME/.nvm/nvm.sh" ]; then export NVM_DIR="$HOME/.nvm" . "$NVM_DIR/nvm.sh" @@ -93,8 +98,12 @@ function populate_config_toml() { fi if [ "$${ARG_ENABLE_AI_GATEWAY}" = "true" ] && [ -n "$${ARG_AIBRIDGE_CONFIG}" ]; then - printf "Adding AI Gateway configuration\n" - echo -e "\n$${ARG_AIBRIDGE_CONFIG}" >> "$${config_path}" + if ! grep -q '\[model_providers\.aibridge\]' "$${config_path}" 2>/dev/null; then + printf "Adding AI Gateway configuration\n" + echo -e "\n$${ARG_AIBRIDGE_CONFIG}" >> "$${config_path}" + else + printf "AI Gateway provider already defined in config, skipping append\n" + fi fi } From c869b978975e3b7d99c46675b596e45b1ca6f1eb Mon Sep 17 00:00:00 2001 From: Jay Kumar Date: Thu, 30 Apr 2026 09:47:12 +0000 Subject: [PATCH 17/25] fix(registry/coder-labs/modules/codex): move npm check after NVM sourcing NVM puts npm on PATH when sourced. The npm existence check must run after the NVM block so users with NVM-managed Node are not rejected. --- .../coder-labs/modules/codex/scripts/install.sh.tftpl | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/registry/coder-labs/modules/codex/scripts/install.sh.tftpl b/registry/coder-labs/modules/codex/scripts/install.sh.tftpl index 1b22793a2..e76707a93 100644 --- a/registry/coder-labs/modules/codex/scripts/install.sh.tftpl +++ b/registry/coder-labs/modules/codex/scripts/install.sh.tftpl @@ -29,15 +29,17 @@ function install_codex() { return fi + if [ -s "$HOME/.nvm/nvm.sh" ]; then + export NVM_DIR="$HOME/.nvm" + . "$NVM_DIR/nvm.sh" + fi + if ! command_exists npm; then echo "Error: npm is required to install Codex. Install Node.js/npm first or set install_codex = false." exit 1 fi - if [ -s "$HOME/.nvm/nvm.sh" ]; then - export NVM_DIR="$HOME/.nvm" - . "$NVM_DIR/nvm.sh" - elif ! command_exists nvm; then + if ! command_exists nvm; then mkdir -p "$HOME/.npm-global" npm config set prefix "$HOME/.npm-global" export PATH="$HOME/.npm-global/bin:$PATH" From 41fa36cecf59a84545723f36fcf3c40908d37830 Mon Sep 17 00:00:00 2001 From: Jay Kumar Date: Mon, 4 May 2026 12:51:25 +0000 Subject: [PATCH 18/25] fix(registry/coder-labs/modules/codex): address all remaining review findings DEREM-1: add model-reasoning-effort-standalone test (without ai_gateway) DEREM-2: rename 'AI Bridge' to 'AI Gateway' in aibridge_config name field DEREM-3: replace tautological var==input tftest assertions with output length checks that verify resource creation DEREM-4: quote path in README coder_app example DEREM-5: document in base_config_toml description that model_reasoning_effort and workdir trust are only applied in defaults DEREM-9: extract collectScripts/runScripts/ModuleScripts into shared agentapi/coder-utils-test-helpers.ts, import in codex tests DEREM-12: add negative assertions to minimal-default-config (no model_provider, no model_providers section, no model_reasoning_effort) DEREM-14: already resolved by DEREM-7 base64 encoding of ARG_WORKDIR DEREM-15: PR description issue (will update separately) DEREM-18: debug header already uses user-facing names (fixed earlier) DEREM-19: container is created by shared setup(); splitting would require a separate code path, accepted as-is since wall time is minimal DEREM-24: add Migrating from v4 section to README --- registry/coder-labs/modules/codex/README.md | 11 +- .../coder-labs/modules/codex/main.test.ts | 109 ++++-------------- registry/coder-labs/modules/codex/main.tf | 4 +- .../coder-labs/modules/codex/main.tftest.hcl | 18 +-- .../modules/codex/scripts/install.sh.tftpl | 2 + .../agentapi/coder-utils-test-helpers.ts | 100 ++++++++++++++++ 6 files changed, 149 insertions(+), 95 deletions(-) create mode 100644 registry/coder/modules/agentapi/coder-utils-test-helpers.ts diff --git a/registry/coder-labs/modules/codex/README.md b/registry/coder-labs/modules/codex/README.md index 62f33c47b..eb1cdb40e 100644 --- a/registry/coder-labs/modules/codex/README.md +++ b/registry/coder-labs/modules/codex/README.md @@ -22,6 +22,15 @@ module "codex" { > [!WARNING] > If upgrading from v4.x.x of this module: v5 is a major refactor that drops support for [Coder Tasks](https://coder.com/docs/ai-coder/tasks) and [Boundary](https://coder.com/docs/ai-coder/agent-firewall). v5 also assumes npm is pre-installed; it no longer bootstraps Node.js. Keep using v4.x.x if you depend on them. +## Migrating from v4 + +1. Remove all v4-only variables: `order`, `group`, `report_tasks`, `subdomain`, `cli_app`, `web_app_display_name`, `cli_app_display_name`, `install_agentapi`, `agentapi_version`, `ai_prompt`, `continue`, `enable_state_persistence`, `codex_system_prompt`, `enable_boundary`, `boundary_config_path`, `boundary_version`, `compile_boundary_from_source`, `use_boundary_directly`, `codex_model`. +2. Rename `enable_aibridge` to `enable_ai_gateway`. +3. Remove any `coder_ai_task` resources that referenced `module.codex.task_app_id`. +4. Add a `coder_app` or `coder_script` to start Codex (v5 only installs and configures the CLI). +5. Ensure npm is available in your workspace image (v5 no longer bootstraps Node.js). +6. Update debug/log paths from `~/.codex-module/` to `~/.coder-modules/coder-labs/codex/logs/`. + ## Examples ### Standalone mode with a launcher app @@ -48,7 +57,7 @@ resource "coder_app" "codex" { command = <<-EOT #!/bin/bash set -e - cd ${local.codex_workdir} + cd "${local.codex_workdir}" codex EOT } diff --git a/registry/coder-labs/modules/codex/main.test.ts b/registry/coder-labs/modules/codex/main.test.ts index 750a9ecd7..0054b8405 100644 --- a/registry/coder-labs/modules/codex/main.test.ts +++ b/registry/coder-labs/modules/codex/main.test.ts @@ -19,54 +19,14 @@ import { extractCoderEnvVars, writeExecutable, } from "../../../coder/modules/agentapi/test-util"; +import { + collectScripts, + runScripts, + ModuleScripts, +} from "../../../coder/modules/agentapi/coder-utils-test-helpers"; import path from "path"; -interface ModuleScripts { - pre_install?: string; - install: string; - post_install?: string; -} - -const SCRIPT_SUFFIXES = [ - "Pre-Install Script", - "Install Script", - "Post-Install Script", -] as const; - -const collectScripts = (state: TerraformState): ModuleScripts => { - const byDisplayName: Record = {}; - for (const resource of state.resources) { - if (resource.type !== "coder_script") continue; - for (const instance of resource.instances) { - const attrs = instance.attributes as Record; - const displayName = attrs.display_name as string | undefined; - const script = attrs.script as string | undefined; - if (displayName && script) { - byDisplayName[displayName] = script; - } - } - } - const scripts: Partial = {}; - for (const suffix of SCRIPT_SUFFIXES) { - const key = `Codex: ${suffix}`; - if (!(key in byDisplayName)) continue; - switch (suffix) { - case "Pre-Install Script": - scripts.pre_install = byDisplayName[key]; - break; - case "Install Script": - scripts.install = byDisplayName[key]; - break; - case "Post-Install Script": - scripts.post_install = byDisplayName[key]; - break; - } - } - if (!scripts.install) { - throw new Error("install script not found in terraform state"); - } - return scripts as ModuleScripts; -}; +const DISPLAY_NAME_PREFIX = "Codex"; let cleanupFunctions: (() => Promise)[] = []; const registerCleanup = (cleanup: () => Promise) => { @@ -104,7 +64,7 @@ const setup = async ( install_codex: "false", ...props?.moduleVariables, }); - const scripts = collectScripts(state); + const scripts = collectScripts(state, DISPLAY_NAME_PREFIX); const coderEnvVars = extractCoderEnvVars(state); const id = await runContainer("codercom/enterprise-node:latest"); @@ -134,43 +94,6 @@ const setup = async ( return { id, coderEnvVars, scripts }; }; -const runScripts = async ( - id: string, - scripts: ModuleScripts, - env?: Record, -) => { - const entries = env ? Object.entries(env) : []; - const envArgs = - entries.length > 0 - ? entries - .map( - ([key, value]) => `export ${key}="${value.replace(/"/g, '\\"')}"`, - ) - .join(" && ") + " && " - : ""; - const ordered: [string, string | undefined][] = [ - ["pre_install", scripts.pre_install], - ["install", scripts.install], - ["post_install", scripts.post_install], - ]; - for (const [name, script] of ordered) { - if (!script) continue; - const target = `/tmp/coder-utils-${name}.sh`; - await writeExecutable({ - containerId: id, - filePath: target, - content: script, - }); - const resp = await execContainer(id, ["bash", "-c", `${envArgs}${target}`]); - if (resp.exitCode !== 0) { - console.log(`script ${name} failed:`); - console.log(resp.stdout); - console.log(resp.stderr); - throw new Error(`coder-utils ${name} script exited ${resp.exitCode}`); - } - } -}; - setDefaultTimeout(60 * 1000); describe("codex", async () => { @@ -260,6 +183,9 @@ describe("codex", async () => { await runScripts(id, scripts); const resp = await readFileContainer(id, "/home/coder/.codex/config.toml"); expect(resp).toContain('preferred_auth_method = "apikey"'); + expect(resp).not.toContain("model_provider"); + expect(resp).not.toContain("[model_providers."); + expect(resp).not.toContain("model_reasoning_effort"); }); test("pre-post-install-scripts", async () => { @@ -316,6 +242,21 @@ describe("codex", async () => { expect(configToml).toContain("[model_providers.aibridge]"); }); + test("model-reasoning-effort-standalone", async () => { + const { id, scripts } = await setup({ + moduleVariables: { + model_reasoning_effort: "high", + }, + }); + await runScripts(id, scripts); + const configToml = await readFileContainer( + id, + "/home/coder/.codex/config.toml", + ); + expect(configToml).toContain('model_reasoning_effort = "high"'); + expect(configToml).not.toContain("model_provider"); + }); + test("workdir-trusted-project", async () => { const workdir = "/home/coder/trusted-project"; const { id, scripts } = await setup({ diff --git a/registry/coder-labs/modules/codex/main.tf b/registry/coder-labs/modules/codex/main.tf index c4a0a3e3d..7d86d4c1c 100644 --- a/registry/coder-labs/modules/codex/main.tf +++ b/registry/coder-labs/modules/codex/main.tf @@ -76,6 +76,8 @@ variable "base_config_toml" { When non-empty, the value is written verbatim as the base of config.toml; additional_mcp_servers and AI Gateway sections are still appended after it. + Note: model_reasoning_effort and workdir trust are only applied in the + default config. Include them in your custom config if needed. EOT default = "" } @@ -127,7 +129,7 @@ locals { workdir = var.workdir != null ? trimsuffix(var.workdir, "/") : "" aibridge_config = <<-EOF [model_providers.aibridge] - name = "AI Bridge" + name = "AI Gateway" base_url = "${data.coder_workspace.me.access_url}/api/v2/aibridge/openai/v1" env_key = "CODER_AIBRIDGE_SESSION_TOKEN" wire_api = "responses" diff --git a/registry/coder-labs/modules/codex/main.tftest.hcl b/registry/coder-labs/modules/codex/main.tftest.hcl index 986823365..13012beda 100644 --- a/registry/coder-labs/modules/codex/main.tftest.hcl +++ b/registry/coder-labs/modules/codex/main.tftest.hcl @@ -43,8 +43,13 @@ run "test_codex_custom_options" { } assert { - condition = var.icon == "/icon/custom.svg" - error_message = "Icon should be set to custom icon" + condition = var.codex_version == "0.1.0" + error_message = "codex_version should be set" + } + + assert { + condition = length(output.scripts) > 0 + error_message = "scripts output should be non-empty with custom options" } } @@ -146,13 +151,8 @@ run "test_codex_with_scripts" { } assert { - condition = var.pre_install_script == "echo 'Pre-install script'" - error_message = "Pre-install script should be set correctly" - } - - assert { - condition = var.post_install_script == "echo 'Post-install script'" - error_message = "Post-install script should be set correctly" + condition = length(output.scripts) == 3 + error_message = "scripts output should have 3 entries when pre/post are configured" } } diff --git a/registry/coder-labs/modules/codex/scripts/install.sh.tftpl b/registry/coder-labs/modules/codex/scripts/install.sh.tftpl index e76707a93..b09abbbb5 100644 --- a/registry/coder-labs/modules/codex/scripts/install.sh.tftpl +++ b/registry/coder-labs/modules/codex/scripts/install.sh.tftpl @@ -21,6 +21,8 @@ echo "--------------------------------" printf "codex_version: %s\n" "$${ARG_CODEX_VERSION}" 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}" echo "--------------------------------" function install_codex() { diff --git a/registry/coder/modules/agentapi/coder-utils-test-helpers.ts b/registry/coder/modules/agentapi/coder-utils-test-helpers.ts new file mode 100644 index 000000000..be389f991 --- /dev/null +++ b/registry/coder/modules/agentapi/coder-utils-test-helpers.ts @@ -0,0 +1,100 @@ +import { execContainer, TerraformState } from "~test"; +import { writeExecutable } from "./test-util"; + +export interface ModuleScripts { + pre_install?: string; + install: string; + post_install?: string; +} + +const SCRIPT_SUFFIXES = [ + "Pre-Install Script", + "Install Script", + "Post-Install Script", +] as const; + +/** + * Extracts coder_script resources from Terraform state, keyed by display name + * prefix. The prefix is the display_name_prefix passed to the coder-utils + * module (e.g. "Codex", "Claude Code"). + */ +export const collectScripts = ( + state: TerraformState, + prefix: string, +): ModuleScripts => { + const byDisplayName: Record = {}; + for (const resource of state.resources) { + if (resource.type !== "coder_script") continue; + for (const instance of resource.instances) { + const attrs = instance.attributes as Record; + const displayName = attrs.display_name as string | undefined; + const script = attrs.script as string | undefined; + if (displayName && script) { + byDisplayName[displayName] = script; + } + } + } + const scripts: Partial = {}; + for (const suffix of SCRIPT_SUFFIXES) { + const key = `${prefix}: ${suffix}`; + if (!(key in byDisplayName)) continue; + switch (suffix) { + case "Pre-Install Script": + scripts.pre_install = byDisplayName[key]; + break; + case "Install Script": + scripts.install = byDisplayName[key]; + break; + case "Post-Install Script": + scripts.post_install = byDisplayName[key]; + break; + } + } + if (!scripts.install) { + throw new Error( + `install script not found in terraform state (looked for "${prefix}: Install Script")`, + ); + } + return scripts as ModuleScripts; +}; + +/** + * Runs the coder-utils script pipeline (pre_install, install, post_install) + * in order inside the container. + */ +export const runScripts = async ( + id: string, + scripts: ModuleScripts, + env?: Record, +) => { + const entries = env ? Object.entries(env) : []; + const envArgs = + entries.length > 0 + ? entries + .map( + ([key, value]) => `export ${key}="${value.replace(/"/g, '\\"')}"`, + ) + .join(" && ") + " && " + : ""; + const ordered: [string, string | undefined][] = [ + ["pre_install", scripts.pre_install], + ["install", scripts.install], + ["post_install", scripts.post_install], + ]; + for (const [name, script] of ordered) { + if (!script) continue; + const target = `/tmp/coder-utils-${name}.sh`; + await writeExecutable({ + containerId: id, + filePath: target, + content: script, + }); + const resp = await execContainer(id, ["bash", "-c", `${envArgs}${target}`]); + if (resp.exitCode !== 0) { + console.log(`script ${name} failed:`); + console.log(resp.stdout); + console.log(resp.stderr); + throw new Error(`coder-utils ${name} script exited ${resp.exitCode}`); + } + } +}; From 2aa54d40c586e42cc4b2929fb356f646f114ef63 Mon Sep 17 00:00:00 2001 From: Jay Kumar Date: Mon, 4 May 2026 13:41:45 +0000 Subject: [PATCH 19/25] revert(registry/coder/modules/agentapi): remove coder-utils-test-helpers.ts, inline helpers back into codex tests --- .../coder-labs/modules/codex/main.test.ts | 91 ++++++++++++++-- .../agentapi/coder-utils-test-helpers.ts | 100 ------------------ 2 files changed, 84 insertions(+), 107 deletions(-) delete mode 100644 registry/coder/modules/agentapi/coder-utils-test-helpers.ts diff --git a/registry/coder-labs/modules/codex/main.test.ts b/registry/coder-labs/modules/codex/main.test.ts index 0054b8405..5ec634977 100644 --- a/registry/coder-labs/modules/codex/main.test.ts +++ b/registry/coder-labs/modules/codex/main.test.ts @@ -19,14 +19,54 @@ import { extractCoderEnvVars, writeExecutable, } from "../../../coder/modules/agentapi/test-util"; -import { - collectScripts, - runScripts, - ModuleScripts, -} from "../../../coder/modules/agentapi/coder-utils-test-helpers"; import path from "path"; -const DISPLAY_NAME_PREFIX = "Codex"; +interface ModuleScripts { + pre_install?: string; + install: string; + post_install?: string; +} + +const SCRIPT_SUFFIXES = [ + "Pre-Install Script", + "Install Script", + "Post-Install Script", +] as const; + +const collectScripts = (state: TerraformState): ModuleScripts => { + const byDisplayName: Record = {}; + for (const resource of state.resources) { + if (resource.type !== "coder_script") continue; + for (const instance of resource.instances) { + const attrs = instance.attributes as Record; + const displayName = attrs.display_name as string | undefined; + const script = attrs.script as string | undefined; + if (displayName && script) { + byDisplayName[displayName] = script; + } + } + } + const scripts: Partial = {}; + for (const suffix of SCRIPT_SUFFIXES) { + const key = `Codex: ${suffix}`; + if (!(key in byDisplayName)) continue; + switch (suffix) { + case "Pre-Install Script": + scripts.pre_install = byDisplayName[key]; + break; + case "Install Script": + scripts.install = byDisplayName[key]; + break; + case "Post-Install Script": + scripts.post_install = byDisplayName[key]; + break; + } + } + if (!scripts.install) { + throw new Error("install script not found in terraform state"); + } + return scripts as ModuleScripts; +}; let cleanupFunctions: (() => Promise)[] = []; const registerCleanup = (cleanup: () => Promise) => { @@ -64,7 +104,7 @@ const setup = async ( install_codex: "false", ...props?.moduleVariables, }); - const scripts = collectScripts(state, DISPLAY_NAME_PREFIX); + const scripts = collectScripts(state); const coderEnvVars = extractCoderEnvVars(state); const id = await runContainer("codercom/enterprise-node:latest"); @@ -94,6 +134,43 @@ const setup = async ( return { id, coderEnvVars, scripts }; }; +const runScripts = async ( + id: string, + scripts: ModuleScripts, + env?: Record, +) => { + const entries = env ? Object.entries(env) : []; + const envArgs = + entries.length > 0 + ? entries + .map( + ([key, value]) => `export ${key}="${value.replace(/"/g, '\\"')}"`, + ) + .join(" && ") + " && " + : ""; + const ordered: [string, string | undefined][] = [ + ["pre_install", scripts.pre_install], + ["install", scripts.install], + ["post_install", scripts.post_install], + ]; + for (const [name, script] of ordered) { + if (!script) continue; + const target = `/tmp/coder-utils-${name}.sh`; + await writeExecutable({ + containerId: id, + filePath: target, + content: script, + }); + const resp = await execContainer(id, ["bash", "-c", `${envArgs}${target}`]); + if (resp.exitCode !== 0) { + console.log(`script ${name} failed:`); + console.log(resp.stdout); + console.log(resp.stderr); + throw new Error(`coder-utils ${name} script exited ${resp.exitCode}`); + } + } +}; + setDefaultTimeout(60 * 1000); describe("codex", async () => { diff --git a/registry/coder/modules/agentapi/coder-utils-test-helpers.ts b/registry/coder/modules/agentapi/coder-utils-test-helpers.ts deleted file mode 100644 index be389f991..000000000 --- a/registry/coder/modules/agentapi/coder-utils-test-helpers.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { execContainer, TerraformState } from "~test"; -import { writeExecutable } from "./test-util"; - -export interface ModuleScripts { - pre_install?: string; - install: string; - post_install?: string; -} - -const SCRIPT_SUFFIXES = [ - "Pre-Install Script", - "Install Script", - "Post-Install Script", -] as const; - -/** - * Extracts coder_script resources from Terraform state, keyed by display name - * prefix. The prefix is the display_name_prefix passed to the coder-utils - * module (e.g. "Codex", "Claude Code"). - */ -export const collectScripts = ( - state: TerraformState, - prefix: string, -): ModuleScripts => { - const byDisplayName: Record = {}; - for (const resource of state.resources) { - if (resource.type !== "coder_script") continue; - for (const instance of resource.instances) { - const attrs = instance.attributes as Record; - const displayName = attrs.display_name as string | undefined; - const script = attrs.script as string | undefined; - if (displayName && script) { - byDisplayName[displayName] = script; - } - } - } - const scripts: Partial = {}; - for (const suffix of SCRIPT_SUFFIXES) { - const key = `${prefix}: ${suffix}`; - if (!(key in byDisplayName)) continue; - switch (suffix) { - case "Pre-Install Script": - scripts.pre_install = byDisplayName[key]; - break; - case "Install Script": - scripts.install = byDisplayName[key]; - break; - case "Post-Install Script": - scripts.post_install = byDisplayName[key]; - break; - } - } - if (!scripts.install) { - throw new Error( - `install script not found in terraform state (looked for "${prefix}: Install Script")`, - ); - } - return scripts as ModuleScripts; -}; - -/** - * Runs the coder-utils script pipeline (pre_install, install, post_install) - * in order inside the container. - */ -export const runScripts = async ( - id: string, - scripts: ModuleScripts, - env?: Record, -) => { - const entries = env ? Object.entries(env) : []; - const envArgs = - entries.length > 0 - ? entries - .map( - ([key, value]) => `export ${key}="${value.replace(/"/g, '\\"')}"`, - ) - .join(" && ") + " && " - : ""; - const ordered: [string, string | undefined][] = [ - ["pre_install", scripts.pre_install], - ["install", scripts.install], - ["post_install", scripts.post_install], - ]; - for (const [name, script] of ordered) { - if (!script) continue; - const target = `/tmp/coder-utils-${name}.sh`; - await writeExecutable({ - containerId: id, - filePath: target, - content: script, - }); - const resp = await execContainer(id, ["bash", "-c", `${envArgs}${target}`]); - if (resp.exitCode !== 0) { - console.log(`script ${name} failed:`); - console.log(resp.stdout); - console.log(resp.stderr); - throw new Error(`coder-utils ${name} script exited ${resp.exitCode}`); - } - } -}; From 8ea4c7729f66c25de3dfabc95326afef30e6c491 Mon Sep 17 00:00:00 2001 From: Jay Kumar Date: Mon, 4 May 2026 14:03:35 +0000 Subject: [PATCH 20/25] fix(registry/coder-labs/modules/codex): rename aibridge to aigateway in config, add pnpm/bun fallbacks - Rename model_provider and [model_providers.] section from 'aibridge' to 'aigateway' across main.tf, install template, tests, and README. The API path and env var stay unchanged (protocol-level). - Add pnpm and bun as fallback package managers for codex installation when npm is not available. --- registry/coder-labs/modules/codex/README.md | 6 ++-- .../coder-labs/modules/codex/main.test.ts | 16 +++++----- registry/coder-labs/modules/codex/main.tf | 6 ++-- .../modules/codex/scripts/install.sh.tftpl | 30 +++++++++++-------- 4 files changed, 32 insertions(+), 26 deletions(-) diff --git a/registry/coder-labs/modules/codex/README.md b/registry/coder-labs/modules/codex/README.md index eb1cdb40e..b71016fcd 100644 --- a/registry/coder-labs/modules/codex/README.md +++ b/registry/coder-labs/modules/codex/README.md @@ -77,16 +77,16 @@ module "codex" { } ``` -When `enable_ai_gateway = true`, the module configures Codex to use the `aibridge` model provider in `config.toml` with the workspace owner's session token for authentication. +When `enable_ai_gateway = true`, the module configures Codex to use the `aigateway` model provider in `config.toml` with the workspace owner's session token for authentication. > [!CAUTION] > `enable_ai_gateway = true` is mutually exclusive with `openai_api_key`. Setting both fails at plan time. > [!NOTE] -> If you provide a custom `base_config_toml`, the module writes it verbatim and does not inject `model_provider = "aibridge"` automatically. Add it to your config yourself: +> If you provide a custom `base_config_toml`, the module writes it verbatim and does not inject `model_provider = "aigateway"` automatically. Add it to your config yourself: > > ```toml -> model_provider = "aibridge" +> model_provider = "aigateway" > ``` ### Advanced Configuration diff --git a/registry/coder-labs/modules/codex/main.test.ts b/registry/coder-labs/modules/codex/main.test.ts index 5ec634977..0d4128c86 100644 --- a/registry/coder-labs/modules/codex/main.test.ts +++ b/registry/coder-labs/modules/codex/main.test.ts @@ -314,9 +314,9 @@ describe("codex", async () => { id, "/home/coder/.codex/config.toml", ); - expect(configToml).toContain('model_provider = "aibridge"'); + expect(configToml).toContain('model_provider = "aigateway"'); expect(configToml).toContain('model_reasoning_effort = "none"'); - expect(configToml).toContain("[model_providers.aibridge]"); + expect(configToml).toContain("[model_providers.aigateway]"); }); test("model-reasoning-effort-standalone", async () => { @@ -367,7 +367,7 @@ describe("codex", async () => { test("ai-gateway-with-custom-base-config", async () => { const baseConfig = [ 'sandbox_mode = "danger-full-access"', - 'model_provider = "aibridge"', + 'model_provider = "aigateway"', ].join("\n"); const { id, coderEnvVars, scripts } = await setup({ moduleVariables: { @@ -380,15 +380,15 @@ describe("codex", async () => { id, "/home/coder/.codex/config.toml", ); - expect(configToml).toContain('model_provider = "aibridge"'); - expect(configToml).toContain("[model_providers.aibridge]"); + expect(configToml).toContain('model_provider = "aigateway"'); + expect(configToml).toContain("[model_providers.aigateway]"); }); test("ai-gateway-custom-config-no-duplicate-provider", async () => { const baseConfig = [ - 'model_provider = "aibridge"', + 'model_provider = "aigateway"', "", - "[model_providers.aibridge]", + "[model_providers.aigateway]", 'name = "Custom AI Bridge"', 'base_url = "https://custom.example.com"', 'env_key = "CODER_AIBRIDGE_SESSION_TOKEN"', @@ -405,7 +405,7 @@ describe("codex", async () => { id, "/home/coder/.codex/config.toml", ); - const matches = configToml.match(/\[model_providers\.aibridge\]/g) || []; + const matches = configToml.match(/\[model_providers\.aigateway\]/g) || []; expect(matches.length).toBe(1); expect(configToml).toContain("Custom AI Bridge"); }); diff --git a/registry/coder-labs/modules/codex/main.tf b/registry/coder-labs/modules/codex/main.tf index 7d86d4c1c..9f315df69 100644 --- a/registry/coder-labs/modules/codex/main.tf +++ b/registry/coder-labs/modules/codex/main.tf @@ -68,7 +68,7 @@ variable "base_config_toml" { When empty, the module generates a minimal default: preferred_auth_method = "apikey" - # model_provider = "aibridge" (sets the default profile, when enable_ai_gateway = true) + # model_provider = "aigateway" (sets the default profile, when enable_ai_gateway = true) # model_reasoning_effort = "" (sets the reasoning effort, when model_reasoning_effort is set) [projects.""] (when workdir is set) @@ -117,7 +117,7 @@ resource "coder_env" "openai_api_key" { } # Authenticates the client against Coder's AI Gateway using the workspace -# owner's session token. Referenced by config.toml model_providers.aibridge. +# owner's session token. Referenced by config.toml model_providers.aigateway. resource "coder_env" "ai_gateway_session_token" { count = var.enable_ai_gateway ? 1 : 0 agent_id = var.agent_id @@ -128,7 +128,7 @@ resource "coder_env" "ai_gateway_session_token" { locals { workdir = var.workdir != null ? trimsuffix(var.workdir, "/") : "" aibridge_config = <<-EOF - [model_providers.aibridge] + [model_providers.aigateway] name = "AI Gateway" base_url = "${data.coder_workspace.me.access_url}/api/v2/aibridge/openai/v1" env_key = "CODER_AIBRIDGE_SESSION_TOKEN" diff --git a/registry/coder-labs/modules/codex/scripts/install.sh.tftpl b/registry/coder-labs/modules/codex/scripts/install.sh.tftpl index b09abbbb5..dfa269e23 100644 --- a/registry/coder-labs/modules/codex/scripts/install.sh.tftpl +++ b/registry/coder-labs/modules/codex/scripts/install.sh.tftpl @@ -36,23 +36,29 @@ function install_codex() { . "$NVM_DIR/nvm.sh" fi - if ! command_exists npm; then - echo "Error: npm is required to install Codex. Install Node.js/npm first or set install_codex = false." + # Detect a package manager for global installs. + if command_exists npm; then + PKG_INSTALL="npm install -g" + if ! command_exists nvm; then + mkdir -p "$HOME/.npm-global" + npm config set prefix "$HOME/.npm-global" + export PATH="$HOME/.npm-global/bin:$PATH" + fi + elif command_exists pnpm; then + PKG_INSTALL="pnpm add -g" + elif command_exists bun; then + PKG_INSTALL="bun add -g" + else + echo "Error: npm, pnpm, or bun is required to install Codex. Install one of them first or set install_codex = false." exit 1 fi - if ! command_exists nvm; then - mkdir -p "$HOME/.npm-global" - npm config set prefix "$HOME/.npm-global" - export PATH="$HOME/.npm-global/bin:$PATH" - fi - printf "%s Installing Codex CLI\n" "$${BOLD}" if [ -n "$${ARG_CODEX_VERSION}" ]; then - npm install -g "@openai/codex@$${ARG_CODEX_VERSION}" + $PKG_INSTALL "@openai/codex@$${ARG_CODEX_VERSION}" else - npm install -g "@openai/codex" + $PKG_INSTALL "@openai/codex" fi printf "%s Installed Codex CLI: %s\n" "$${BOLD}" "$(codex --version)" } @@ -62,7 +68,7 @@ function write_minimal_default_config() { local optional_config="" if [ "$${ARG_ENABLE_AI_GATEWAY}" = "true" ]; then - optional_config='model_provider = "aibridge"' + optional_config='model_provider = "aigateway"' fi if [ -n "$${ARG_MODEL_REASONING_EFFORT}" ]; then @@ -102,7 +108,7 @@ function populate_config_toml() { fi if [ "$${ARG_ENABLE_AI_GATEWAY}" = "true" ] && [ -n "$${ARG_AIBRIDGE_CONFIG}" ]; then - if ! grep -q '\[model_providers\.aibridge\]' "$${config_path}" 2>/dev/null; then + if ! grep -q '\[model_providers\.aigateway\]' "$${config_path}" 2>/dev/null; then printf "Adding AI Gateway configuration\n" echo -e "\n$${ARG_AIBRIDGE_CONFIG}" >> "$${config_path}" else From 8adff57035754aafb9117764e09ac2f4c12ae9c4 Mon Sep 17 00:00:00 2001 From: Jay Kumar Date: Tue, 5 May 2026 03:18:42 +0000 Subject: [PATCH 21/25] fix(registry/coder-labs/modules/codex): restore PATH persistence and auth.json seeding Issue 1: Port ensure_codex_in_path and add_path_to_shell_profiles from claude-code v5. Symlinks codex into CODER_SCRIPT_BIN_DIR and persists the binary dir to shell profiles (bash, zsh, fish). Called after both the install and skip-install branches. Issue 3: Restore add_auth_json from v4. Writes OPENAI_API_KEY to ~/.codex/auth.json when AI Gateway is not enabled, fixing 401s on Codex versions where the env var alone is insufficient. Issue 6: Add README note warning that coder_app commands re-execute on pane reconnect; recommend coder_script for one-shot prompts. --- registry/coder-labs/modules/codex/README.md | 3 + registry/coder-labs/modules/codex/main.tf | 1 + .../modules/codex/scripts/install.sh.tftpl | 65 +++++++++++++++++++ 3 files changed, 69 insertions(+) diff --git a/registry/coder-labs/modules/codex/README.md b/registry/coder-labs/modules/codex/README.md index b71016fcd..c4c746245 100644 --- a/registry/coder-labs/modules/codex/README.md +++ b/registry/coder-labs/modules/codex/README.md @@ -63,6 +63,9 @@ resource "coder_app" "codex" { } ``` +> [!NOTE] +> The `coder_app` command re-executes on every pane reconnect. This works for interactive `codex` (which stays alive), but one-shot commands like `codex exec` will re-run each time. For one-shot prompts, use a `coder_script` (runs once at startup) and a `coder_app` that attaches to the existing session (e.g. via tmux/screen). + ### Usage with 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. diff --git a/registry/coder-labs/modules/codex/main.tf b/registry/coder-labs/modules/codex/main.tf index 9f315df69..7aebd487b 100644 --- a/registry/coder-labs/modules/codex/main.tf +++ b/registry/coder-labs/modules/codex/main.tf @@ -144,6 +144,7 @@ locals { 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 + ARG_OPENAI_API_KEY = var.openai_api_key != "" ? base64encode(var.openai_api_key) : "" }) module_dir_name = ".coder-modules/coder-labs/codex" } diff --git a/registry/coder-labs/modules/codex/scripts/install.sh.tftpl b/registry/coder-labs/modules/codex/scripts/install.sh.tftpl index dfa269e23..eeb5d5ff1 100644 --- a/registry/coder-labs/modules/codex/scripts/install.sh.tftpl +++ b/registry/coder-labs/modules/codex/scripts/install.sh.tftpl @@ -16,6 +16,7 @@ ARG_ADDITIONAL_MCP_SERVERS=$(echo -n '${ARG_ADDITIONAL_MCP_SERVERS}' | 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}' +ARG_OPENAI_API_KEY=$(echo -n '${ARG_OPENAI_API_KEY}' | base64 -d) echo "--------------------------------" printf "codex_version: %s\n" "$${ARG_CODEX_VERSION}" @@ -25,9 +26,55 @@ printf "install_codex: %s\n" "$${ARG_INSTALL}" printf "model_reasoning_effort: %s\n" "$${ARG_MODEL_REASONING_EFFORT}" echo "--------------------------------" +function 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}" ]; then + if ! grep -q "$${path_dir}" "$${profile}" 2> /dev/null; then + echo "export PATH=\"\$PATH:$${path_dir}\"" >> "$${profile}" + echo "Added $${path_dir} to $${profile}" + fi + fi + done + + local fish_config="$HOME/.config/fish/config.fish" + if [ -f "$${fish_config}" ]; then + if ! grep -q "$${path_dir}" "$${fish_config}" 2> /dev/null; then + echo "fish_add_path $${path_dir}" >> "$${fish_config}" + echo "Added $${path_dir} to $${fish_config}" + fi + fi +} + +function ensure_codex_in_path() { + local CODEX_BIN="" + if command -v codex > /dev/null 2>&1; then + CODEX_BIN=$(command -v codex) + elif [ -x "$HOME/.npm-global/bin/codex" ]; then + CODEX_BIN="$HOME/.npm-global/bin/codex" + fi + + if [ -z "$${CODEX_BIN}" ] || [ ! -x "$${CODEX_BIN}" ]; then + echo "Warning: Could not find codex binary after install" + return + fi + + local CODEX_DIR + CODEX_DIR=$(dirname "$${CODEX_BIN}") + + if [ -n "$${CODER_SCRIPT_BIN_DIR:-}" ] && [ ! -e "$${CODER_SCRIPT_BIN_DIR}/codex" ]; then + ln -s "$${CODEX_BIN}" "$${CODER_SCRIPT_BIN_DIR}/codex" + echo "Created symlink: $${CODER_SCRIPT_BIN_DIR}/codex -> $${CODEX_BIN}" + fi + + add_path_to_shell_profiles "$${CODEX_DIR}" +} + function install_codex() { if [ "$${ARG_INSTALL}" != "true" ]; then echo "Skipping Codex installation as per configuration." + ensure_codex_in_path return fi @@ -61,6 +108,7 @@ function install_codex() { $PKG_INSTALL "@openai/codex" fi printf "%s Installed Codex CLI: %s\n" "$${BOLD}" "$(codex --version)" + ensure_codex_in_path } function write_minimal_default_config() { @@ -124,6 +172,23 @@ function setup_workdir() { fi } +function add_auth_json() { + if [ "$${ARG_ENABLE_AI_GATEWAY}" = "true" ] || [ -z "$${ARG_OPENAI_API_KEY}" ]; then + return + fi + + local auth_path="$HOME/.codex/auth.json" + mkdir -p "$(dirname "$${auth_path}")" + + cat << EOF > "$${auth_path}" +{ + "OPENAI_API_KEY": "$${ARG_OPENAI_API_KEY}" +} +EOF + echo "Seeded auth.json with API key" +} + install_codex populate_config_toml setup_workdir +add_auth_json From a39ed9e1c1e3ba688b7b1a0c2ef98be9149bb468 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Tue, 5 May 2026 09:16:44 +0530 Subject: [PATCH 22/25] feat(install): add auth_mode configuration for API key usage --- registry/coder-labs/modules/codex/scripts/install.sh.tftpl | 1 + 1 file changed, 1 insertion(+) diff --git a/registry/coder-labs/modules/codex/scripts/install.sh.tftpl b/registry/coder-labs/modules/codex/scripts/install.sh.tftpl index eeb5d5ff1..887efd9f3 100644 --- a/registry/coder-labs/modules/codex/scripts/install.sh.tftpl +++ b/registry/coder-labs/modules/codex/scripts/install.sh.tftpl @@ -182,6 +182,7 @@ function add_auth_json() { cat << EOF > "$${auth_path}" { + "auth_mode": "apikey", "OPENAI_API_KEY": "$${ARG_OPENAI_API_KEY}" } EOF From c14bf63fcbcfebf19bf7ff41002a684c76c7a86f Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Tue, 5 May 2026 10:14:49 +0530 Subject: [PATCH 23/25] docs(README): update link to Codex configuration documentation --- registry/coder-labs/modules/codex/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/registry/coder-labs/modules/codex/README.md b/registry/coder-labs/modules/codex/README.md index c4c746245..3b10497c7 100644 --- a/registry/coder-labs/modules/codex/README.md +++ b/registry/coder-labs/modules/codex/README.md @@ -149,7 +149,7 @@ resource "coder_script" "post_codex" { ## Configuration -When no custom `base_config_toml` is provided, the module uses a minimal default with `preferred_auth_method = "apikey"`. For advanced options, see [Codex config docs](https://github.com/openai/codex/blob/main/codex-rs/config.md). +When no custom `base_config_toml` is provided, the module uses a minimal default with `preferred_auth_method = "apikey"`. For advanced options, see [Codex config docs](https://github.com/openai/codex/blob/main/docs/config.md). ## Troubleshooting From bc26defac4a090be6655f23c445036b6ed519250 Mon Sep 17 00:00:00 2001 From: Jay Kumar Date: Tue, 5 May 2026 13:39:40 +0000 Subject: [PATCH 24/25] docs(registry/coder-labs/modules/codex): remove caller guidance from README intro --- registry/coder-labs/modules/codex/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/registry/coder-labs/modules/codex/README.md b/registry/coder-labs/modules/codex/README.md index 3b10497c7..ba1b756fa 100644 --- a/registry/coder-labs/modules/codex/README.md +++ b/registry/coder-labs/modules/codex/README.md @@ -8,7 +8,7 @@ tags: [agent, codex, ai, openai, ai-gateway] # Codex CLI -Install and configure the [Codex CLI](https://github.com/openai/codex) in your workspace. Starting Codex is left to the caller (template command, IDE launcher, or a custom `coder_script`). +Install and configure the [Codex CLI](https://github.com/openai/codex) in your workspace. ```tf module "codex" { From a8ceee02acb060b032b95be3d7be16774875ca1d Mon Sep 17 00:00:00 2001 From: Jay Kumar Date: Tue, 5 May 2026 14:44:50 +0000 Subject: [PATCH 25/25] fix(registry/coder-labs/modules/codex): address matifali and DevelopmentCats review comments - Move migration guide from README to PR body, link from warning - Update codex_version example to 0.128.0 (latest) - Update config docs URL to developers.openai.com/codex/config-advanced - Restore AI Gateway docs link in References - Remove remaining tautological tftest assertions (DEREM-26) - Add custom-config-drops-reasoning-effort test (DEREM-29) - DEREM-31 already fixed by ensure_codex_in_path - DEREM-27/28 acknowledged, no change needed --- registry/coder-labs/modules/codex/README.md | 16 ++++---------- .../coder-labs/modules/codex/main.test.ts | 20 ++++++++++++++++++ .../coder-labs/modules/codex/main.tftest.hcl | 21 +++---------------- 3 files changed, 27 insertions(+), 30 deletions(-) diff --git a/registry/coder-labs/modules/codex/README.md b/registry/coder-labs/modules/codex/README.md index ba1b756fa..e524e6323 100644 --- a/registry/coder-labs/modules/codex/README.md +++ b/registry/coder-labs/modules/codex/README.md @@ -20,16 +20,7 @@ module "codex" { ``` > [!WARNING] -> If upgrading from v4.x.x of this module: v5 is a major refactor that drops support for [Coder Tasks](https://coder.com/docs/ai-coder/tasks) and [Boundary](https://coder.com/docs/ai-coder/agent-firewall). v5 also assumes npm is pre-installed; it no longer bootstraps Node.js. Keep using v4.x.x if you depend on them. - -## Migrating from v4 - -1. Remove all v4-only variables: `order`, `group`, `report_tasks`, `subdomain`, `cli_app`, `web_app_display_name`, `cli_app_display_name`, `install_agentapi`, `agentapi_version`, `ai_prompt`, `continue`, `enable_state_persistence`, `codex_system_prompt`, `enable_boundary`, `boundary_config_path`, `boundary_version`, `compile_boundary_from_source`, `use_boundary_directly`, `codex_model`. -2. Rename `enable_aibridge` to `enable_ai_gateway`. -3. Remove any `coder_ai_task` resources that referenced `module.codex.task_app_id`. -4. Add a `coder_app` or `coder_script` to start Codex (v5 only installs and configures the CLI). -5. Ensure npm is available in your workspace image (v5 no longer bootstraps Node.js). -6. Update debug/log paths from `~/.codex-module/` to `~/.coder-modules/coder-labs/codex/logs/`. +> If upgrading from v4.x.x of this module: v5 is a major refactor that drops support for [Coder Tasks](https://coder.com/docs/ai-coder/tasks) and [Boundary](https://coder.com/docs/ai-coder/agent-firewall). v5 also assumes npm is pre-installed; it no longer bootstraps Node.js. Keep using v4.x.x if you depend on them. See the [PR description](https://github.com/coder/registry/pull/879) for a full migration guide. ## Examples @@ -102,7 +93,7 @@ module "codex" { workdir = "/home/coder/project" openai_api_key = var.openai_api_key - codex_version = "0.1.0" + codex_version = "0.128.0" base_config_toml = <<-EOT sandbox_mode = "danger-full-access" @@ -149,7 +140,7 @@ resource "coder_script" "post_codex" { ## Configuration -When no custom `base_config_toml` is provided, the module uses a minimal default with `preferred_auth_method = "apikey"`. For advanced options, see [Codex config docs](https://github.com/openai/codex/blob/main/docs/config.md). +When no custom `base_config_toml` is provided, the module uses a minimal default with `preferred_auth_method = "apikey"`. For advanced options, see [Codex config docs](https://developers.openai.com/codex/config-advanced). ## Troubleshooting @@ -164,3 +155,4 @@ cat ~/.coder-modules/coder-labs/codex/logs/post_install.log ## References - [Codex CLI Documentation](https://github.com/openai/codex) +- [AI Gateway](https://coder.com/docs/ai-coder/ai-gateway) diff --git a/registry/coder-labs/modules/codex/main.test.ts b/registry/coder-labs/modules/codex/main.test.ts index 0d4128c86..8e7e514c8 100644 --- a/registry/coder-labs/modules/codex/main.test.ts +++ b/registry/coder-labs/modules/codex/main.test.ts @@ -424,4 +424,24 @@ describe("codex", async () => { ); expect(installLog).toContain("Installed Codex CLI"); }); + + test("custom-config-drops-reasoning-effort", async () => { + const baseConfig = [ + 'sandbox_mode = "danger-full-access"', + 'preferred_auth_method = "apikey"', + ].join("\n"); + const { id, scripts } = await setup({ + moduleVariables: { + base_config_toml: baseConfig, + model_reasoning_effort: "high", + }, + }); + await runScripts(id, scripts); + const configToml = await readFileContainer( + id, + "/home/coder/.codex/config.toml", + ); + expect(configToml).toContain('sandbox_mode = "danger-full-access"'); + expect(configToml).not.toContain("model_reasoning_effort"); + }); }); diff --git a/registry/coder-labs/modules/codex/main.tftest.hcl b/registry/coder-labs/modules/codex/main.tftest.hcl index 13012beda..3bcd681ac 100644 --- a/registry/coder-labs/modules/codex/main.tftest.hcl +++ b/registry/coder-labs/modules/codex/main.tftest.hcl @@ -6,11 +6,6 @@ run "test_codex_basic" { workdir = "/home/coder" } - assert { - condition = var.workdir == "/home/coder" - error_message = "Workdir should be set correctly" - } - assert { condition = var.install_codex == true error_message = "install_codex should default to true" @@ -39,12 +34,7 @@ run "test_codex_custom_options" { agent_id = "test-agent" workdir = "/home/coder/project" icon = "/icon/custom.svg" - codex_version = "0.1.0" - } - - assert { - condition = var.codex_version == "0.1.0" - error_message = "codex_version should be set" + codex_version = "0.128.0" } assert { @@ -69,11 +59,6 @@ run "test_ai_gateway_enabled" { } } - assert { - condition = var.enable_ai_gateway == true - error_message = "AI Gateway should be enabled" - } - assert { condition = coder_env.ai_gateway_session_token[0].name == "CODER_AIBRIDGE_SESSION_TOKEN" error_message = "CODER_AIBRIDGE_SESSION_TOKEN should be set" @@ -194,7 +179,7 @@ run "test_workdir_optional" { } assert { - condition = var.workdir == null - error_message = "workdir should default to null when omitted" + condition = length(output.scripts) == 1 + error_message = "scripts output should have install script even without workdir" } }