feat(codex): bump CLI to 0.130.0 + consume CODEX_AUTH_JSON subscription OAuth (#6)
publish-image / Resolve runtime version (push) Successful in 4s
publish-image / Build & push workspace-template-codex image (push) Failing after 33s
CI / Template validation (static) (push) Successful in 44s
CI / Adapter unit tests (push) Successful in 1m3s
CI / Template validation (runtime) (push) Successful in 39s
CI / T4 tier-4 conformance (live) (push) Successful in 1m25s
CI / validate (push) Successful in 1s

This commit was merged in pull request #6.
This commit is contained in:
2026-05-18 04:32:56 +00:00
parent 858b0937e1
commit 283f371095
6 changed files with 138 additions and 47 deletions
+20 -6
View File
@@ -115,12 +115,26 @@ RUN chmod +x /usr/local/bin/start.sh \
# --- Install the OpenAI Codex CLI globally as root (binary lives in
# /usr/lib/node_modules and symlinks into /usr/bin/codex; available to
# both root and the agent user). Pinned to the 0.57 line — the exact
# release range proven working in the live-verified prod image
# (codex-cli 0.57.0; MiniMax `chat` WireApi + app-server protocol).
# codex's app-server protocol is `experimental` and breaks across
# minor versions — bump deliberately when validating a new release.
RUN npm install -g @openai/codex@~0.57
# both root and the agent user).
#
# Pinned EXACTLY to 0.130.0 (not a `~`/`^` range). Rationale:
# * 0.130.0 is the npm `latest` dist-tag — the current stable line
# (0.131.x is alpha-only at the time of this change; we do not
# ship a pre-release CLI in a prod runtime image).
# * The previous `~0.57` pin PREDATES `codex login --device-auth` /
# ChatGPT-subscription OAuth: it cannot consume the modern
# `auth.json` shape ({auth_mode:"chatgpt", tokens:{id_token,
# access_token,refresh_token,account_id}, last_refresh}) and
# ignores `forced_login_method = "chatgpt"`. The subscription
# OAuth credential we now materialize (see start.sh Mode C) is
# only usable on a CLI that supports this format — 0.130.0 does.
# * config.yaml's default model (`gpt-5.5`) and the May-2026 roster
# were already live-verified against codex-cli 0.130.0
# linux/amd64 (thread/start returned "model":"gpt-5.5").
# * codex's app-server protocol is `experimental` and breaks across
# minor versions, so we pin the EXACT patch release rather than a
# range — a bump is a deliberate, reviewed, re-verified change.
RUN npm install -g @openai/codex@0.130.0
USER agent
WORKDIR /home/agent
+15 -3
View File
@@ -28,14 +28,26 @@ molecule-core for the full design rationale.
| `app_server.py` | `AppServerProcess` — async JSON-RPC over NDJSON stdio against the codex app-server child |
| `tests/` | 12 unit tests covering both modules; `mock_app_server.py` is a Python NDJSON stand-in for the real `codex` binary |
| `config.yaml` | Runtime config — model list (OpenAI-only), required env, A2A wiring |
| `Dockerfile` | python:3.11-slim + Node.js 20 + `npm i -g @openai/codex@^0.72` + molecule_runtime |
| `start.sh` | Verifies codex binary + OPENAI_API_KEY, then exec's molecule-runtime |
| `Dockerfile` | python:3.11-slim + Node.js 20 + `npm i -g @openai/codex@0.130.0` (exact pin) + molecule_runtime |
| `start.sh` | Verifies codex binary, materializes the ChatGPT/Codex-subscription `auth.json` (Mode C), then exec's molecule-runtime |
## Auth (codex resolves any ONE of these)
Codex needs exactly one credential. Resolution order mirrors OpenClaw's
`openai-codex` provider — an injected subscription `auth.json` is
preferred over the pay-as-you-go API key:
| Credential | How it's supplied | Notes |
|---|---|---|
| `CODEX_AUTH_JSON` | Workspace Config-tab secret bound from Infisical SSOT `/shared/codex-oauth` key `CODEX_AUTH_JSON` (env=prod). `start.sh` writes it to `~/.codex/auth.json` (0600, agent-owned) + sets `cli_auth_credentials_store = "file"` / `forced_login_method = "chatgpt"`. | **Preferred.** ChatGPT/Codex *subscription* OAuth (`auth_mode:"chatgpt"`). SINGLE-RUNNER only — never fan out across concurrent workspaces. `CODEX_CHATGPT_AUTH_JSON` is a backward-compat alias (PR #5); `CODEX_AUTH_JSON` wins if both set. Requires codex CLI ≥ the 0.13x line (this image pins 0.130.0); the legacy 0.57 line cannot consume this format. |
| `OPENAI_API_KEY` | Config-tab env | **Documented fallback.** Pay-as-you-go OpenAI platform key. Retained, not removed. |
| `MINIMAX_API_KEY` | Config-tab env | MiniMax chat-wire route (`codex_minimax_config.sh`). |
## Required env
| Variable | Required | Notes |
|---|---|---|
| `OPENAI_API_KEY` | Yes | Codex is OpenAI-only |
| one codex credential | Yes | `CODEX_AUTH_JSON` (preferred) **or** `OPENAI_API_KEY` (fallback) **or** `MINIMAX_API_KEY` — see Auth table |
| `MOLECULE_PLATFORM_URL` | Yes | Standard molecule-runtime |
| `MOLECULE_WORKSPACE_ID` | Yes | Standard molecule-runtime |
+15 -8
View File
@@ -83,10 +83,17 @@ class CodexAdapter(BaseAdapter):
# A. OPENAI_API_KEY — direct OpenAI path (codex default).
# B. MINIMAX_API_KEY — MiniMax chat-wire route
# (codex_minimax_config.sh writes config.toml).
# C. $CODEX_HOME/auth.json — an injected
# ChatGPT-subscription credential (auth_mode:chatgpt),
# written by start.sh from CODEX_CHATGPT_AUTH_JSON for a
# SINGLE runner. Codex prefers auth.json over env keys.
# C. $CODEX_HOME/auth.json — an injected ChatGPT/Codex
# -subscription credential (auth_mode:"chatgpt"),
# materialized by start.sh from the CODEX_AUTH_JSON env
# var (Infisical SSOT /shared/codex-oauth, key
# CODEX_AUTH_JSON, env=prod; CODEX_CHATGPT_AUTH_JSON is a
# backward-compat alias) for a SINGLE runner. This mirrors
# OpenClaw's openai-codex auth.order: prefer an injected
# subscription auth.json over the pay-as-you-go API key.
# Codex prefers auth.json over env keys. The
# OPENAI_API_KEY path (A) is retained as the documented
# fallback and is intentionally NOT removed.
# CODEX_HOME defaults to ~/.codex; honor an explicit override
# so a non-default home is still detected.
codex_home = os.environ.get("CODEX_HOME") or os.path.join(
@@ -103,10 +110,10 @@ class CodexAdapter(BaseAdapter):
"No codex credential found. Codex needs exactly one "
"of: OPENAI_API_KEY (direct OpenAI), MINIMAX_API_KEY "
"(MiniMax chat-wire route), or an injected "
"ChatGPT-subscription auth.json at "
f"{auth_json} (set CODEX_CHATGPT_AUTH_JSON for a "
"single-runner workspace). Configure via the canvas "
"Config tab."
"ChatGPT/Codex-subscription auth.json at "
f"{auth_json} (set CODEX_AUTH_JSON — the Infisical "
"/shared/codex-oauth credential — for a single-runner "
"workspace). Configure via the canvas Config tab."
)
async def create_executor(self, config: AdapterConfig):
+7 -5
View File
@@ -8,8 +8,10 @@ molecule-ai-workspace-runtime>=0.1.0
#
# MiniMax provider is wired via a static config.toml written by
# codex_minimax_config.sh — no proxy or translation layer needed
# (codex 0.57's WireApi enum natively supports both `responses` and
# `chat`; MiniMax uses `chat`). The Dockerfile pins @openai/codex
# to ~0.57 — the exact release line live-verified in prod (the
# app-server protocol is `experimental` and breaks across minors;
# bump deliberately when validating a new release).
# (the codex WireApi enum natively supports both `responses` and
# `chat`; MiniMax uses `chat`). The Dockerfile pins @openai/codex to
# the exact patch release 0.130.0 — the npm `latest` stable line and
# the first pinned line that supports ChatGPT/Codex-subscription
# OAuth auth.json (the prior ~0.57 pin predated it). The app-server
# protocol is `experimental` and breaks across minors; bump
# deliberately, with a re-verification, when validating a new release.
+34 -19
View File
@@ -46,7 +46,7 @@ echo "----- start.sh boot $(date -u +%Y-%m-%dT%H:%M:%SZ) -----"
echo "uid=$(id -u) gid=$(id -g) user=$(id -un 2>/dev/null || echo unknown)"
echo "workspace_id=${WORKSPACE_ID:-<unset>} platform_url=${PLATFORM_URL:-<unset>}"
echo "configs_dir: $(ls -ld /configs 2>/dev/null || echo MISSING)"
for var in OPENAI_API_KEY MINIMAX_API_KEY KIMI_API_KEY MOLECULE_ORG_ID; do
for var in OPENAI_API_KEY MINIMAX_API_KEY KIMI_API_KEY CODEX_AUTH_JSON CODEX_CHATGPT_AUTH_JSON MOLECULE_ORG_ID; do
eval "val=\${$var:-}"
if [ -n "${val:-}" ]; then echo "env $var=set"; else echo "env $var=unset"; fi
done
@@ -93,30 +93,45 @@ elif [ -f /app/codex_mcp_config.sh ]; then
HOME=/home/agent CODEX_HOME=/home/agent/.codex \
bash /app/codex_mcp_config.sh
fi
# --- Mode C: headless ChatGPT-subscription auth (single-runner only) ---
# When CODEX_CHATGPT_AUTH_JSON is set (the CONTENTS of a codex
# `auth.json`, auth_mode:chatgpt, injected via the workspace Config tab
# secret for EXACTLY ONE runner — the future combined Reviewer+
# Researcher box on the CTO's ChatGPT subscription), write it to
# $CODEX_HOME/auth.json so codex authenticates off the subscription
# instead of OPENAI_API_KEY. Codex's documented headless refresh
# (refresh-and-retry on 401, rewrites auth.json in place) handles token
# rotation; the persistent /home/agent volume keeps the refreshed file.
# We deliberately add NO refresh daemon — OpenAI's supported CI/CD
# pattern is "run codex and persist the updated auth.json", not a
# manual refresh endpoint (RFC §5).
# --- Mode C: headless ChatGPT/Codex-subscription auth (single-runner) ---
# Canonical credential: CODEX_AUTH_JSON. This is the CONTENTS of a
# codex `auth.json` (auth_mode:"chatgpt", OPENAI_API_KEY:null,
# tokens:{id_token,access_token,refresh_token,account_id},
# last_refresh) — the OpenClaw `openai-codex` provider's auth.order
# pattern (docs.openclaw.ai/providers/openai): prefer an injected
# subscription auth.json over a pay-as-you-go API key. The blob is
# stored in the self-hosted Infisical SSOT at secret path
# `/shared/codex-oauth`, key `CODEX_AUTH_JSON` (env=prod), and is
# injected into the workspace container as the CODEX_AUTH_JSON env
# var via the workspace Config-tab secret binding for EXACTLY ONE
# runner (the combined Reviewer+Researcher box on the CTO's
# ChatGPT/Codex subscription).
#
# Inert when CODEX_CHATGPT_AUTH_JSON is unset: the OPENAI_API_KEY and
# MiniMax paths above are byte-unchanged. This is SINGLE-RUNNER only;
# there is intentionally no multi-workspace credential fanout (RFC §5,
# §8) — one auth.json per runner, never shared across concurrent jobs.
if [ -n "${CODEX_CHATGPT_AUTH_JSON:-}" ]; then
# CODEX_CHATGPT_AUTH_JSON is accepted as a DOCUMENTED backward-compat
# alias (the name shipped by template PR #5). CODEX_AUTH_JSON wins if
# both are set, so a Config-tab override can shadow a stale alias.
#
# Writing it to $CODEX_HOME/auth.json makes codex authenticate off the
# subscription instead of OPENAI_API_KEY. Codex's documented headless
# refresh (refresh-and-retry on 401, rewrites auth.json in place)
# handles token rotation; the persistent /home/agent volume keeps the
# refreshed file. We deliberately add NO refresh daemon — OpenAI's
# supported CI/CD pattern is "run codex and persist the updated
# auth.json", not a manual refresh endpoint (RFC §5).
#
# Inert when neither var is set: the OPENAI_API_KEY and MiniMax paths
# above are byte-unchanged and remain the DOCUMENTED FALLBACK. This is
# SINGLE-RUNNER only; there is intentionally no multi-workspace
# credential fanout (RFC §5, §8) — one auth.json per runner, never
# shared across concurrent jobs. The token is never echoed.
CODEX_AUTH_BLOB="${CODEX_AUTH_JSON:-${CODEX_CHATGPT_AUTH_JSON:-}}"
if [ -n "${CODEX_AUTH_BLOB}" ]; then
CODEX_HOME_DIR="/home/agent/.codex"
install -d -o agent -g agent "$CODEX_HOME_DIR"
AUTH_JSON_PATH="${CODEX_HOME_DIR}/auth.json"
# Write the injected contents verbatim. printf %s avoids any
# interpretation of backslashes/format chars in the token blob.
printf '%s' "${CODEX_CHATGPT_AUTH_JSON}" > "$AUTH_JSON_PATH"
printf '%s' "${CODEX_AUTH_BLOB}" > "$AUTH_JSON_PATH"
chown agent:agent "$AUTH_JSON_PATH"
chmod 0600 "$AUTH_JSON_PATH"
# Ensure codex reads file-backed credentials (not the OS keyring,
+47 -6
View File
@@ -11,8 +11,10 @@ Three groups:
2. adapter.setup() accepts auth.json as a third credential (mode C)
and still fails closed when nothing is set.
3. start.sh writes/omits ~/.codex/auth.json + config.toml keys
correctly based on CODEX_CHATGPT_AUTH_JSON (structural; mode C is
verified structurally only — we do not hold a real CTO auth.json).
correctly based on CODEX_AUTH_JSON (canonical Infisical key) and
its CODEX_CHATGPT_AUTH_JSON backward-compat alias (structural;
mode C is verified structurally only — we do not exercise a real
subscription round-trip in CI).
"""
from __future__ import annotations
@@ -143,11 +145,12 @@ _MODE_C_PROBE = r"""
set -euo pipefail
mkdir -p /home/agent && export HOME=/home/agent
# Inline the exact mode-C block from start.sh.
if [ -n "${CODEX_CHATGPT_AUTH_JSON:-}" ]; then
CODEX_AUTH_BLOB="${CODEX_AUTH_JSON:-${CODEX_CHATGPT_AUTH_JSON:-}}"
if [ -n "${CODEX_AUTH_BLOB}" ]; then
CODEX_HOME_DIR="/home/agent/.codex"
mkdir -p "$CODEX_HOME_DIR"
AUTH_JSON_PATH="${CODEX_HOME_DIR}/auth.json"
printf '%s' "${CODEX_CHATGPT_AUTH_JSON}" > "$AUTH_JSON_PATH"
printf '%s' "${CODEX_AUTH_BLOB}" > "$AUTH_JSON_PATH"
chmod 0600 "$AUTH_JSON_PATH"
CONFIG_TOML="${CODEX_HOME_DIR}/config.toml"
touch "$CONFIG_TOML"
@@ -163,14 +166,19 @@ fi
def _start_sh_has_mode_c() -> bool:
txt = (_ROOT / "start.sh").read_text()
return "CODEX_CHATGPT_AUTH_JSON" in txt and "cli_auth_credentials_store" in txt
return "CODEX_AUTH_JSON" in txt and "cli_auth_credentials_store" in txt
def test_start_sh_contains_mode_c_block() -> None:
"""Guard: the real start.sh carries the mode-C wiring + the
single-runner intent + the preflight third-credential branch."""
txt = (_ROOT / "start.sh").read_text()
# Canonical Infisical key (/shared/codex-oauth key CODEX_AUTH_JSON)
assert "CODEX_AUTH_JSON" in txt
# backward-compat alias still recognized (PR #5 name)
assert "CODEX_CHATGPT_AUTH_JSON" in txt
# canonical key must take precedence over the alias
assert '${CODEX_AUTH_JSON:-${CODEX_CHATGPT_AUTH_JSON:-}}' in txt
assert 'cli_auth_credentials_store = "file"' in txt
assert 'forced_login_method = "chatgpt"' in txt
assert "single-runner" in txt.lower()
@@ -178,6 +186,16 @@ def test_start_sh_contains_mode_c_block() -> None:
assert ".codex/auth.json" in txt
def test_codex_cli_pinned_to_0130_exact() -> None:
"""The Dockerfile must pin @openai/codex to the exact 0.130.0
patch — the stable line that supports subscription-OAuth
auth.json. A range pin or the legacy 0.57 line is a regression."""
df = (_ROOT / "Dockerfile").read_text()
assert "npm install -g @openai/codex@0.130.0" in df
assert "@openai/codex@~0.57" not in df
assert "@openai/codex@^0.72" not in df
def _run_probe(env: dict) -> Path:
home = Path(env["__TMP_HOME"])
script = _MODE_C_PROBE.replace("/home/agent", str(home))
@@ -191,8 +209,9 @@ def _run_probe(env: dict) -> Path:
def test_mode_c_writes_auth_json_and_config_keys(tmp_path) -> None:
"""Canonical path: CODEX_AUTH_JSON (the Infisical key)."""
codex_dir = _run_probe({
"CODEX_CHATGPT_AUTH_JSON": '{"auth_mode":"chatgpt","tokens":{}}',
"CODEX_AUTH_JSON": '{"auth_mode":"chatgpt","tokens":{}}',
"__TMP_HOME": str(tmp_path),
})
auth = codex_dir / "auth.json"
@@ -205,6 +224,28 @@ def test_mode_c_writes_auth_json_and_config_keys(tmp_path) -> None:
assert 'forced_login_method = "chatgpt"' in body
def test_mode_c_alias_still_works(tmp_path) -> None:
"""Backward-compat: the PR #5 CODEX_CHATGPT_AUTH_JSON name still
materializes auth.json when the canonical var is unset."""
codex_dir = _run_probe({
"CODEX_CHATGPT_AUTH_JSON": '{"auth_mode":"chatgpt","alias":1}',
"__TMP_HOME": str(tmp_path),
})
assert (codex_dir / "auth.json").read_text() == \
'{"auth_mode":"chatgpt","alias":1}'
def test_mode_c_canonical_wins_over_alias(tmp_path) -> None:
"""If both are set, CODEX_AUTH_JSON must shadow the alias so a
Config-tab override can supersede a stale value."""
codex_dir = _run_probe({
"CODEX_AUTH_JSON": '{"src":"canonical"}',
"CODEX_CHATGPT_AUTH_JSON": '{"src":"alias"}',
"__TMP_HOME": str(tmp_path),
})
assert (codex_dir / "auth.json").read_text() == '{"src":"canonical"}'
def test_mode_c_is_inert_when_env_unset(tmp_path) -> None:
codex_dir = _run_probe({"__TMP_HOME": str(tmp_path)})
assert not (codex_dir / "auth.json").exists()