From d5775fe98870f4d7ba7cf322bd05283533079aa3 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 13 May 2026 22:20:27 -0700 Subject: [PATCH] feat(codex-runtime): skip unavailable plugins during migration (#25437) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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."@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). --- hermes_cli/codex_runtime_plugin_migration.py | 16 ++++++ .../test_codex_runtime_plugin_migration.py | 50 ++++++++++++++++++- .../features/codex-app-server-runtime.md | 3 +- 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/hermes_cli/codex_runtime_plugin_migration.py b/hermes_cli/codex_runtime_plugin_migration.py index c00ec26bd29..dd7faa09794 100644 --- a/hermes_cli/codex_runtime_plugin_migration.py +++ b/hermes_cli/codex_runtime_plugin_migration.py @@ -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 diff --git a/tests/hermes_cli/test_codex_runtime_plugin_migration.py b/tests/hermes_cli/test_codex_runtime_plugin_migration.py index 0274251327c..b2e27f8c97b 100644 --- a/tests/hermes_cli/test_codex_runtime_plugin_migration.py +++ b/tests/hermes_cli/test_codex_runtime_plugin_migration.py @@ -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.""" diff --git a/website/docs/user-guide/features/codex-app-server-runtime.md b/website/docs/user-guide/features/codex-app-server-runtime.md index 5d4b068088b..a1aa6a0776e 100644 --- a/website/docs/user-guide/features/codex-app-server-runtime.md +++ b/website/docs/user-guide/features/codex-app-server-runtime.md @@ -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.