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
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:
+20
-6
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user