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.