From 373f78f91c38df236cfbec469ebad9f7c9e30891 Mon Sep 17 00:00:00 2001 From: eddymarc <132223353+Edd88-pixel@users.noreply.github.com> Date: Mon, 24 Aug 2026 17:09:27 +0100 Subject: [PATCH 1/3] fix(auth): keep API keys out of rendered scripts --- registry/coder-labs/modules/codex/README.md | 14 +- .../coder-labs/modules/codex/main.test.ts | 225 ++++++++++++++++-- registry/coder-labs/modules/codex/main.tf | 1 - .../coder-labs/modules/codex/main.tftest.hcl | 5 + .../modules/codex/scripts/install.sh.tftpl | 91 ++++--- .../modules/codex/testdata/codex-mock.sh | 7 + 6 files changed, 293 insertions(+), 50 deletions(-) diff --git a/registry/coder-labs/modules/codex/README.md b/registry/coder-labs/modules/codex/README.md index 0fbe241d7..c8a3660d2 100644 --- a/registry/coder-labs/modules/codex/README.md +++ b/registry/coder-labs/modules/codex/README.md @@ -13,7 +13,7 @@ Install and configure the [Codex CLI](https://github.com/openai/codex) in your w ```tf module "codex" { source = "registry.coder.com/coder-labs/codex/coder" - version = "5.3.1" + version = "5.3.2" agent_id = coder_agent.main.id openai_api_key = var.openai_api_key } @@ -33,7 +33,7 @@ locals { module "codex" { source = "registry.coder.com/coder-labs/codex/coder" - version = "5.3.1" + version = "5.3.2" agent_id = coder_agent.main.id workdir = local.codex_workdir openai_api_key = var.openai_api_key @@ -64,7 +64,7 @@ resource "coder_app" "codex" { ```tf module "codex" { source = "registry.coder.com/coder-labs/codex/coder" - version = "5.3.1" + version = "5.3.2" agent_id = coder_agent.main.id workdir = "/home/coder/project" enable_ai_gateway = true @@ -88,7 +88,7 @@ When `enable_ai_gateway = true`, the module configures Codex to use the `aigatew ```tf module "codex" { source = "registry.coder.com/coder-labs/codex/coder" - version = "5.3.1" + version = "5.3.2" agent_id = coder_agent.main.id workdir = "/home/coder/project" openai_api_key = var.openai_api_key @@ -134,7 +134,7 @@ The module exposes the `scripts` output: an ordered list of `coder exp sync` nam ```tf module "codex" { source = "registry.coder.com/coder-labs/codex/coder" - version = "5.3.1" + version = "5.3.2" agent_id = coder_agent.main.id openai_api_key = var.openai_api_key } @@ -159,6 +159,10 @@ resource "coder_script" "post_codex" { 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). +When `openai_api_key` is set, the module authenticates with `codex login --with-api-key` over standard input. The key remains in the `OPENAI_API_KEY` workspace environment variable and is not rendered into the install script or written to `auth.json` by the module. + +If `install_codex = false`, a working `codex` executable must already be available on `PATH`. Workspace startup fails if the binary is missing or `codex --version` cannot run. + > [!NOTE] > Content you add outside the managed block is preserved across workspace restarts. The module structures the file as: > diff --git a/registry/coder-labs/modules/codex/main.test.ts b/registry/coder-labs/modules/codex/main.test.ts index ce7b2a313..c5057f487 100644 --- a/registry/coder-labs/modules/codex/main.test.ts +++ b/registry/coder-labs/modules/codex/main.test.ts @@ -148,6 +148,15 @@ const runScripts = async ( ) .join(" && ") + " && " : ""; + const runRenderedScript = async (name: string, script: string) => { + const target = `/tmp/coder-utils-${name}.sh`; + await writeExecutable({ + containerId: id, + filePath: target, + content: script, + }); + return execContainer(id, ["bash", "-c", `${envArgs}${target}`]); + }; const ordered: [string, string | undefined][] = [ ["pre_install", scripts.pre_install], ["install", scripts.install], @@ -155,13 +164,7 @@ const runScripts = async ( ]; 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}`]); + const resp = await runRenderedScript(name, script); if (resp.exitCode !== 0) { console.log(`script ${name} failed:`); console.log(resp.stdout); @@ -171,6 +174,80 @@ const runScripts = async ( } }; +const runInstallScript = async ( + id: string, + script: string, + env?: Record, +) => { + const entries = env ? Object.entries(env) : []; + const envArgs = entries + .map(([key, value]) => `export ${key}="${value.replace(/"/g, '\\"')}"`) + .join(" && "); + const target = "/tmp/coder-utils-install.sh"; + await writeExecutable({ containerId: id, filePath: target, content: script }); + return execContainer(id, [ + "bash", + "-c", + `${envArgs ? `${envArgs} && ` : ""}${target}`, + ]); +}; + +const CODEX_TARGET = "x86_64-unknown-linux-musl"; + +const writeCodexArchive = async ( + id: string, + options?: { binaryName?: string; binaryContent?: string }, +) => { + const binaryName = options?.binaryName ?? `codex-${CODEX_TARGET}`; + await execContainer(id, [ + "bash", + "-c", + "rm -rf /tmp/codex-fixture && mkdir -p /tmp/codex-fixture", + ]); + await writeExecutable({ + containerId: id, + filePath: `/tmp/codex-fixture/${binaryName}`, + content: + options?.binaryContent ?? + '#!/bin/bash\nif [[ "$1" == "--version" ]]; then echo "codex test version"; exit 0; fi\nexit 0\n', + }); + const archive = await execContainer(id, [ + "tar", + "-czf", + "/tmp/codex-test-archive.tar.gz", + "-C", + "/tmp/codex-fixture", + binaryName, + ]); + expect(archive.exitCode).toBe(0); +}; + +const writeCurlMock = async (id: string, exitCode = 0) => { + await writeExecutable({ + containerId: id, + filePath: "/usr/local/bin/curl", + content: [ + "#!/bin/bash", + "printf '%s\\n' \"$*\" > /tmp/codex-curl-args", + "output=''", + "while (($#)); do", + ' case "$1" in', + ' --output|-o) output="$2"; shift 2 ;;', + " *) shift ;;", + " esac", + "done", + `if [[ ${exitCode} -ne 0 ]]; then exit ${exitCode}; fi`, + 'cp /tmp/codex-test-archive.tar.gz "$output"', + ].join("\n"), + }); +}; + +const installLog = (id: string) => + readFileContainer( + id, + "/home/coder/.coder-modules/coder-labs/codex/logs/install.log", + ); + const MANAGED_START = "# >>> coder-managed: codex module >>>"; const MANAGED_END = "# <<< coder-managed: codex module <<<"; @@ -200,22 +277,70 @@ describe("codex", async () => { codex_version: version, }, }); + await writeCodexArchive(id); + await writeCurlMock(id); await runScripts(id, scripts, coderEnvVars); - const installLog = await readFileContainer( - id, - "/home/coder/.coder-modules/coder-labs/codex/logs/install.log", - ); - expect(installLog).toContain(version); + const log = await installLog(id); + const curlArgs = await readFileContainer(id, "/tmp/codex-curl-args"); + expect(log).toContain("Installed Codex CLI: codex test version"); + expect(curlArgs).toContain(`rust-v${version}/codex-${CODEX_TARGET}.tar.gz`); + expect(curlArgs).toContain("--retry 2"); + expect(curlArgs).toContain("--connect-timeout 10"); + expect(curlArgs).toContain("--max-time 60"); }); test("openai-api-key", async () => { const apiKey = "test-api-key-123"; - const { coderEnvVars } = await setup({ + const encodedApiKey = Buffer.from(apiKey).toString("base64"); + const { id, coderEnvVars, scripts } = await setup({ moduleVariables: { openai_api_key: apiKey, }, }); expect(coderEnvVars["OPENAI_API_KEY"]).toBe(apiKey); + expect(scripts.install).not.toContain(apiKey); + expect(scripts.install).not.toContain(encodedApiKey); + + await runScripts(id, scripts, coderEnvVars); + expect(await readFileContainer(id, "/tmp/codex-login-stdin")).toBe(apiKey); + const log = await installLog(id); + expect(log).toContain("codex invoked with: login --with-api-key"); + expect(log).toContain("Codex authenticated successfully."); + expect(log).not.toContain(apiKey); + expect( + ( + await execContainer(id, [ + "bash", + "-c", + "test ! -e /home/coder/.codex/auth.json", + ]) + ).exitCode, + ).toBe(0); + }); + + test("openai-api-key-authentication-failure-is-terminal", async () => { + const apiKey = "test-api-key-failure"; + const { id, coderEnvVars, scripts } = await setup({ + moduleVariables: { openai_api_key: apiKey }, + }); + const result = await runInstallScript(id, scripts.install, { + ...coderEnvVars, + CODEX_LOGIN_EXIT_CODE: "7", + }); + expect(result.exitCode).not.toBe(0); + const log = await installLog(id); + expect(log).toContain("Codex authentication failed."); + expect(log).not.toContain("Codex authenticated successfully."); + expect(log).not.toContain(apiKey); + }); + + test("preinstalled-codex-is-required-when-installation-is-disabled", async () => { + const { id, scripts } = await setup({ skipCodexMock: true }); + const result = await runInstallScript(id, scripts.install); + expect(result.exitCode).not.toBe(0); + const log = await installLog(id); + expect(log).toContain("Codex binary was not found or is not executable."); + expect(log).not.toContain("Validated existing Codex CLI"); }); test("base-config-toml", async () => { @@ -424,12 +549,78 @@ describe("codex", async () => { install_codex: "true", }, }); + await writeCodexArchive(id); + await writeCurlMock(id); await runScripts(id, scripts, coderEnvVars); - const installLog = await readFileContainer( - id, - "/home/coder/.coder-modules/coder-labs/codex/logs/install.log", + const log = await installLog(id); + const curlArgs = await readFileContainer(id, "/tmp/codex-curl-args"); + expect(log).toContain("Installed Codex CLI: codex test version"); + expect(curlArgs).toContain( + `releases/latest/download/codex-${CODEX_TARGET}.tar.gz`, ); - expect(installLog).toContain("Installed Codex CLI"); + }); + + test("codex-download-failure-is-terminal", async () => { + const { id, scripts } = await setup({ + skipCodexMock: true, + moduleVariables: { install_codex: "true" }, + }); + await writeCurlMock(id, 28); + const result = await runInstallScript(id, scripts.install); + expect(result.exitCode).not.toBe(0); + const log = await installLog(id); + expect(log).toContain( + "Codex could not be downloaded after up to 3 attempts.", + ); + expect(log).not.toContain("Installed Codex CLI"); + }); + + test("invalid-codex-archive-is-rejected", async () => { + const { id, scripts } = await setup({ + skipCodexMock: true, + moduleVariables: { install_codex: "true" }, + }); + await writeExecutable({ + containerId: id, + filePath: "/tmp/codex-test-archive.tar.gz", + content: "not a tar archive", + }); + await writeCurlMock(id); + const result = await runInstallScript(id, scripts.install); + expect(result.exitCode).not.toBe(0); + const log = await installLog(id); + expect(log).toContain("Codex download was not a valid archive."); + expect(log).not.toContain("Installed Codex CLI"); + }); + + test("codex-archive-must-contain-expected-binary", async () => { + const { id, scripts } = await setup({ + skipCodexMock: true, + moduleVariables: { install_codex: "true" }, + }); + await writeCodexArchive(id, { binaryName: "unexpected-codex" }); + await writeCurlMock(id); + const result = await runInstallScript(id, scripts.install); + expect(result.exitCode).not.toBe(0); + const log = await installLog(id); + expect(log).toContain("Codex archive did not contain the expected binary."); + expect(log).not.toContain("Installed Codex CLI"); + }); + + test("downloaded-codex-binary-must-run", async () => { + const { id, scripts } = await setup({ + skipCodexMock: true, + moduleVariables: { install_codex: "true" }, + }); + await writeCodexArchive(id, { + binaryContent: "#!/bin/bash\nexit 7\n", + }); + await writeCurlMock(id); + const result = await runInstallScript(id, scripts.install); + expect(result.exitCode).not.toBe(0); + const log = await installLog(id); + expect(log).toContain("Downloaded Codex binary could not be executed."); + expect(log).not.toContain("Installed Codex CLI"); }); test("mcp-config-remote-path", async () => { diff --git a/registry/coder-labs/modules/codex/main.tf b/registry/coder-labs/modules/codex/main.tf index 36ecf2a38..7674d1b21 100644 --- a/registry/coder-labs/modules/codex/main.tf +++ b/registry/coder-labs/modules/codex/main.tf @@ -151,7 +151,6 @@ 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/main.tftest.hcl b/registry/coder-labs/modules/codex/main.tftest.hcl index 08507e218..e3f3c9716 100644 --- a/registry/coder-labs/modules/codex/main.tftest.hcl +++ b/registry/coder-labs/modules/codex/main.tftest.hcl @@ -25,6 +25,11 @@ run "test_codex_with_api_key" { condition = coder_env.openai_api_key[0].value == "test-key" error_message = "OpenAI API key should be set correctly" } + + assert { + condition = !strcontains(local.install_script, nonsensitive(var.openai_api_key)) && !strcontains(local.install_script, base64encode(nonsensitive(var.openai_api_key))) + error_message = "OpenAI API key should not be rendered into the install script" + } } run "test_codex_custom_options" { diff --git a/registry/coder-labs/modules/codex/scripts/install.sh.tftpl b/registry/coder-labs/modules/codex/scripts/install.sh.tftpl index 05c3d94af..66d676096 100644 --- a/registry/coder-labs/modules/codex/scripts/install.sh.tftpl +++ b/registry/coder-labs/modules/codex/scripts/install.sh.tftpl @@ -3,6 +3,7 @@ set -euo pipefail BOLD='\033[0;1m' +CODEX_VERSION="" command_exists() { command -v "$1" > /dev/null 2>&1 @@ -17,7 +18,6 @@ ARG_MCP_CONFIG_REMOTE_PATH=$(echo -n '${ARG_MCP_CONFIG_REMOTE_PATH}' | base64 -d ARG_ENABLE_AI_GATEWAY='${ARG_ENABLE_AI_GATEWAY}' ARG_AIBRIDGE_CONFIG=$(echo -n '${ARG_AIBRIDGE_CONFIG}' | base64 -d) ARG_MODEL_REASONING_EFFORT='${ARG_MODEL_REASONING_EFFORT}' -ARG_OPENAI_API_KEY=$(echo -n '${ARG_OPENAI_API_KEY}' | base64 -d) echo "--------------------------------" printf "codex_version: %s\n" "$${ARG_CODEX_VERSION}" @@ -56,8 +56,13 @@ function ensure_codex_in_path() { fi if [ -z "$${CODEX_BIN}" ] || [ ! -x "$${CODEX_BIN}" ]; then - echo "Warning: Could not find codex binary after install" - return + echo "Codex binary was not found or is not executable." >&2 + return 1 + fi + + if ! CODEX_VERSION=$("$${CODEX_BIN}" --version); then + echo "Codex binary could not be executed." >&2 + return 1 fi local CODEX_DIR @@ -71,10 +76,55 @@ function ensure_codex_in_path() { add_path_to_shell_profiles "$${CODEX_DIR}" } +function download_codex() ( + local target="$1" + local url="$2" + local tmp_dir archive archive_contents candidate + + tmp_dir=$(mktemp -d) + trap 'rm -rf "$${tmp_dir}"' EXIT + archive="$${tmp_dir}/codex.tar.gz" + archive_contents="$${tmp_dir}/archive-contents" + candidate="$${tmp_dir}/codex-$${target}" + + if ! curl --fail --silent --show-error --location \ + --retry 2 --retry-delay 1 --retry-all-errors \ + --connect-timeout 10 --max-time 60 \ + --output "$${archive}" "$${url}"; then + echo "Codex could not be downloaded after up to 3 attempts." >&2 + return 1 + fi + + if ! tar -tzf "$${archive}" > "$${archive_contents}" 2> /dev/null; then + echo "Codex download was not a valid archive." >&2 + return 1 + fi + + if ! grep -qx "codex-$${target}" "$${archive_contents}"; then + echo "Codex archive did not contain the expected binary." >&2 + return 1 + fi + + if ! tar -xzf "$${archive}" -C "$${tmp_dir}" "codex-$${target}"; then + echo "Codex archive could not be extracted." >&2 + return 1 + fi + + chmod +x "$${candidate}" + if ! "$${candidate}" --version > /dev/null; then + echo "Downloaded Codex binary could not be executed." >&2 + return 1 + fi + + mkdir -p "$HOME/.local/bin" + mv "$${candidate}" "$HOME/.local/bin/codex" +) + function install_codex() { if [ "$${ARG_INSTALL}" != "true" ]; then echo "Skipping Codex installation as per configuration." ensure_codex_in_path + printf "%s Validated existing Codex CLI: %s\n" "$${BOLD}" "$${CODEX_VERSION}" return fi @@ -92,19 +142,11 @@ function install_codex() { fi printf "Downloading %s\n" "$${url}" - local tmp_dir - tmp_dir=$(mktemp -d) - curl -fsSL -o "$${tmp_dir}/codex.tar.gz" "$${url}" - - tar -xzf "$${tmp_dir}/codex.tar.gz" -C "$${tmp_dir}" - mkdir -p "$HOME/.local/bin" - mv "$${tmp_dir}/codex-$${target}" "$HOME/.local/bin/codex" - chmod +x "$HOME/.local/bin/codex" - rm -rf "$${tmp_dir}" + download_codex "$${target}" "$${url}" export PATH="$HOME/.local/bin:$PATH" - printf "%s Installed Codex CLI: %s\n" "$${BOLD}" "$(codex --version)" ensure_codex_in_path + printf "%s Installed Codex CLI: %s\n" "$${BOLD}" "$${CODEX_VERSION}" } function write_minimal_default_config() { @@ -189,7 +231,7 @@ function populate_config_toml() { fi if [ "$${ARG_ENABLE_AI_GATEWAY}" = "true" ] && [ -n "$${ARG_AIBRIDGE_CONFIG}" ]; then - if ! grep -q '\[model_providers\.aigateway\]' "$${managed}" 2>/dev/null; then + if ! grep -q '\[model_providers\.aigateway\]' "$${managed}" 2> /dev/null; then printf "Adding AI Gateway configuration\n" printf '\n%s\n' "$${ARG_AIBRIDGE_CONFIG}" >> "$${managed}" else @@ -247,24 +289,19 @@ function setup_workdir() { fi } -function add_auth_json() { - if [ "$${ARG_ENABLE_AI_GATEWAY}" = "true" ] || [ -z "$${ARG_OPENAI_API_KEY}" ]; then +function authenticate_codex() { + if [ "$${ARG_ENABLE_AI_GATEWAY}" = "true" ] || [ -z "$${OPENAI_API_KEY:-}" ]; then return fi - local auth_path="$HOME/.codex/auth.json" - mkdir -p "$(dirname "$${auth_path}")" - - cat << EOF > "$${auth_path}" -{ - "auth_mode": "apikey", - "OPENAI_API_KEY": "$${ARG_OPENAI_API_KEY}" -} -EOF - echo "Seeded auth.json with API key" + if ! printf '%s' "$${OPENAI_API_KEY}" | codex login --with-api-key; then + echo "Codex authentication failed." >&2 + return 1 + fi + echo "Codex authenticated successfully." } install_codex populate_config_toml setup_workdir -add_auth_json +authenticate_codex diff --git a/registry/coder-labs/modules/codex/testdata/codex-mock.sh b/registry/coder-labs/modules/codex/testdata/codex-mock.sh index a73b70b5c..c3d70f85f 100644 --- a/registry/coder-labs/modules/codex/testdata/codex-mock.sh +++ b/registry/coder-labs/modules/codex/testdata/codex-mock.sh @@ -5,5 +5,12 @@ if [[ "$1" == "--version" ]]; then exit 0 fi +if [[ "$1" == "login" && "$2" == "--with-api-key" ]]; then + umask 077 + cat > /tmp/codex-login-stdin + echo "codex invoked with: login --with-api-key" + exit "${CODEX_LOGIN_EXIT_CODE:-0}" +fi + echo "codex invoked with: $*" exit 0 From bfba8525d6c39f66bc99e059d1ab1c652697e657 Mon Sep 17 00:00:00 2001 From: Eddy Marc <132223353+Edd88-pixel@users.noreply.github.com> Date: Wed, 26 Aug 2026 20:20:56 +0100 Subject: [PATCH 2/3] Update registry/coder-labs/modules/codex/scripts/install.sh.tftpl Co-authored-by: DevCats --- registry/coder-labs/modules/codex/scripts/install.sh.tftpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/registry/coder-labs/modules/codex/scripts/install.sh.tftpl b/registry/coder-labs/modules/codex/scripts/install.sh.tftpl index 66d676096..822d26048 100644 --- a/registry/coder-labs/modules/codex/scripts/install.sh.tftpl +++ b/registry/coder-labs/modules/codex/scripts/install.sh.tftpl @@ -89,7 +89,7 @@ function download_codex() ( if ! curl --fail --silent --show-error --location \ --retry 2 --retry-delay 1 --retry-all-errors \ - --connect-timeout 10 --max-time 60 \ + --connect-timeout 10 --max-time 300 \ --output "$${archive}" "$${url}"; then echo "Codex could not be downloaded after up to 3 attempts." >&2 return 1 From 1b2812a2094a008281710d192c445efd411a7bd7 Mon Sep 17 00:00:00 2001 From: eddymarc <132223353+Edd88-pixel@users.noreply.github.com> Date: Wed, 26 Aug 2026 20:27:54 +0100 Subject: [PATCH 3/3] test(codex): align download timeout assertion --- registry/coder-labs/modules/codex/main.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/registry/coder-labs/modules/codex/main.test.ts b/registry/coder-labs/modules/codex/main.test.ts index c5057f487..c71d9a795 100644 --- a/registry/coder-labs/modules/codex/main.test.ts +++ b/registry/coder-labs/modules/codex/main.test.ts @@ -286,7 +286,7 @@ describe("codex", async () => { expect(curlArgs).toContain(`rust-v${version}/codex-${CODEX_TARGET}.tar.gz`); expect(curlArgs).toContain("--retry 2"); expect(curlArgs).toContain("--connect-timeout 10"); - expect(curlArgs).toContain("--max-time 60"); + expect(curlArgs).toContain("--max-time 300"); }); test("openai-api-key", async () => {