feat(codex-runtime): skip unavailable plugins during migration (#25437)

Followup to PR #24182 — caught when scanning OpenClaw for recent codex
fixes we hadn't considered. OpenClaw learned the hard way (#80815) that
migrating plugins which codex itself reports as unavailable produces
config that fails at activation time.

Our /codex-runtime codex_app_server enable path queries codex's
plugin/list and migrates everything where installed=true. We were
trusting codex's installation state and ignoring its availability
field. So a plugin that's installed=true but availability=UNAVAILABLE
(broken local install) or REQUIRES_AUTH (OAuth expired or never
completed) would get an [plugins."<n>@openai-curated"] entry in
~/.codex/config.toml — and the user's first codex turn after enabling
the runtime would fail because codex refuses to activate it.

Fix: filter on availability in _query_codex_plugins(). Only emit
plugins where availability is empty (older codex versions without the
field — preserve backward compat) or explicitly AVAILABLE.

Tests:
  test_plugin_discovery_skips_unavailable_plugins — verifies 4 cases:
    - good-plugin (installed=True, availability=AVAILABLE) → migrated
    - broken-plugin (installed=True, availability=UNAVAILABLE) → skipped
    - auth-pending (installed=True, availability=REQUIRES_AUTH) → skipped
    - legacy-plugin (installed=True, no availability field) → migrated
      (older codex versions; preserve backward compat)

Docs:
  Added bullet to 'What's NOT migrated' list in the docs page calling
  out the availability filter and why.

Other OpenClaw codex PRs I reviewed but did NOT apply (with reasoning):
  - #81591 (load Codex for selectable models): we resolve runtime
    per-call already, no startup-time gating to fix
  - #81510 (cron compatibility): we documented cron as untested; their
    fix is for OpenClaw-specific cron orchestration shape
  - #81223 (rotate incompatible context-engine threads): we don't
    have a Lossless context engine equivalent
  - #80688 (constrain sandbox): we don't have an outer-sandbox concept
  - #80616 (release on turn_aborted): we already handle status=
    interrupted in turn/completed correctly
  - #80278 (expose activeModel in plugin SDK): not our surface
  - #80792 (default destructive_actions on): we don't expose that knob

56 codex-runtime migration tests still green (+1 new).
This commit is contained in:
Teknium 2026-05-13 22:20:27 -07:00 committed by GitHub
parent f7ad2f1115
commit d5775fe988
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 67 additions and 2 deletions

View file

@ -397,6 +397,22 @@ def _query_codex_plugins(
installed = bool(plugin.get("installed", False))
if not installed:
continue
# Skip plugins codex itself reports as unavailable (broken
# install, missing OAuth, removed from marketplace, etc.).
# Cf. openclaw/openclaw#80815 — OpenClaw learned to gate
# migration on app readiness to avoid writing config that
# would fail at activation time. Our migration writes to
# codex's config.toml directly, so a broken plugin would
# surface as a codex error on first use. Skipping it here
# keeps the migrated config clean and the user's first
# codex turn from failing.
availability = str(plugin.get("availability") or "").upper()
if availability and availability != "AVAILABLE":
logger.debug(
"skipping plugin %s: availability=%s",
plugin.get("name"), availability,
)
continue
name = str(plugin.get("name") or "")
if not name:
continue

View file

@ -353,7 +353,7 @@ class TestMigrate:
], None
monkeypatch.setattr(crpm, "_query_codex_plugins", fake_query)
report = migrate({}, codex_home=tmp_path, discover_plugins=True, expose_hermes_tools=False)
report = migrate({}, codex_home=tmp_path, discover_plugins=True)
text = (tmp_path / "config.toml").read_text()
assert '[plugins."github@openai-curated"]' in text
assert '[plugins."google-calendar@openai-curated"]' in text
@ -361,6 +361,54 @@ class TestMigrate:
assert "google-calendar@openai-curated" in report.migrated_plugins
assert "github@openai-curated" in report.migrated_plugins
def test_plugin_discovery_skips_unavailable_plugins(self):
"""Plugins where codex reports availability != AVAILABLE should
be skipped they're broken/uninstallable on codex's side, so
migrating them would write config that fails at activation
time. Cf. openclaw#80815."""
from hermes_cli.codex_runtime_plugin_migration import _query_codex_plugins
from unittest.mock import patch
# Fake a plugin/list response where one plugin is unavailable
fake_response = {
"marketplaces": [{
"name": "openai-curated",
"plugins": [
{"name": "good-plugin", "installed": True,
"enabled": True, "availability": "AVAILABLE"},
{"name": "broken-plugin", "installed": True,
"enabled": True, "availability": "UNAVAILABLE"},
{"name": "auth-pending", "installed": True,
"enabled": True, "availability": "REQUIRES_AUTH"},
# Plugin without availability field — pass through
# (older codex versions or marketplaces that don't
# set it should still work).
{"name": "legacy-plugin", "installed": True,
"enabled": True},
]
}]
}
class FakeClient:
def __init__(self, **kw): pass
def initialize(self, **kw): pass
def request(self, method, params, timeout=None):
return fake_response
def close(self): pass
def __enter__(self): return self
def __exit__(self, *a): pass
with patch("agent.transports.codex_app_server.CodexAppServerClient",
FakeClient):
plugins, err = _query_codex_plugins()
assert err is None
names = [p["name"] for p in plugins]
assert "good-plugin" in names
assert "legacy-plugin" in names # no field → don't skip
assert "broken-plugin" not in names
assert "auth-pending" not in names
def test_plugin_discovery_failure_non_fatal(self, tmp_path, monkeypatch):
"""If codex isn't installed or RPC fails, MCP migration still
completes. The error surfaces in the report but doesn't abort."""

View file

@ -340,7 +340,8 @@ Plugins installed via `codex plugin` (Linear, GitHub, Gmail, Calendar, Canva, et
This means: when your friend says "I have Calendar and GitHub set up in my Codex CLI" and they enable Hermes' codex runtime, Hermes activates those automatically. No re-configuration needed.
What's NOT migrated:
- Plugins not yet installed in Codex CLI. Install them via `codex plugin` first.
- Plugins you haven't installed yet — install them in Codex first.
- Plugins where codex reports `availability != AVAILABLE` (broken install, expired OAuth, removed from marketplace, etc.). These are skipped to avoid writing config that would fail at activation time.
- ChatGPT app marketplace entries (the per-account `app/list` results — these are already enabled inside codex by virtue of your account auth).
- Plugin OAuth — you authorize each plugin once in Codex itself; Hermes doesn't touch credentials.