diff --git a/agent/auxiliary_client.py b/agent/auxiliary_client.py index 95d5def0ac..7cb8f9f52e 100644 --- a/agent/auxiliary_client.py +++ b/agent/auxiliary_client.py @@ -849,7 +849,7 @@ def _resolve_forced_provider(forced: str) -> Tuple[Optional[OpenAI], Optional[st if forced == "nous": client, model = _try_nous() if client is None: - logger.warning("auxiliary.provider=nous but Nous Portal not configured (run: hermes login)") + logger.warning("auxiliary.provider=nous but Nous Portal not configured (run: hermes auth)") return client, model if forced == "codex": @@ -1119,7 +1119,7 @@ def resolve_provider_client( client, default = _try_nous() if client is None: logger.warning("resolve_provider_client: nous requested " - "but Nous Portal not configured (run: hermes login)") + "but Nous Portal not configured (run: hermes auth)") return None, None final_model = model or default return (_to_async_client(client, final_model) if async_mode diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py index bfbeb81825..fd65246a33 100644 --- a/hermes_cli/auth.py +++ b/hermes_cli/auth.py @@ -936,7 +936,7 @@ def _read_codex_tokens(*, _lock: bool = True) -> Dict[str, Any]: state = _load_provider_state(auth_store, "openai-codex") if not state: raise AuthError( - "No Codex credentials stored. Run `hermes login` to authenticate.", + "No Codex credentials stored. Run `hermes auth` to authenticate.", provider="openai-codex", code="codex_auth_missing", relogin_required=True, @@ -944,7 +944,7 @@ def _read_codex_tokens(*, _lock: bool = True) -> Dict[str, Any]: tokens = state.get("tokens") if not isinstance(tokens, dict): raise AuthError( - "Codex auth state is missing tokens. Run `hermes login` to re-authenticate.", + "Codex auth state is missing tokens. Run `hermes auth` to re-authenticate.", provider="openai-codex", code="codex_auth_invalid_shape", relogin_required=True, @@ -953,14 +953,14 @@ def _read_codex_tokens(*, _lock: bool = True) -> Dict[str, Any]: refresh_token = tokens.get("refresh_token") if not isinstance(access_token, str) or not access_token.strip(): raise AuthError( - "Codex auth is missing access_token. Run `hermes login` to re-authenticate.", + "Codex auth is missing access_token. Run `hermes auth` to re-authenticate.", provider="openai-codex", code="codex_auth_missing_access_token", relogin_required=True, ) if not isinstance(refresh_token, str) or not refresh_token.strip(): raise AuthError( - "Codex auth is missing refresh_token. Run `hermes login` to re-authenticate.", + "Codex auth is missing refresh_token. Run `hermes auth` to re-authenticate.", provider="openai-codex", code="codex_auth_missing_refresh_token", relogin_required=True, @@ -995,7 +995,7 @@ def refresh_codex_oauth_pure( del access_token # Access token is only used by callers to decide whether to refresh. if not isinstance(refresh_token, str) or not refresh_token.strip(): raise AuthError( - "Codex auth is missing refresh_token. Run `hermes login` to re-authenticate.", + "Codex auth is missing refresh_token. Run `hermes auth` to re-authenticate.", provider="openai-codex", code="codex_auth_missing_refresh_token", relogin_required=True, @@ -1035,7 +1035,7 @@ def refresh_codex_oauth_pure( "Codex refresh token was already consumed by another client " "(e.g. Codex CLI or VS Code extension). " "Run `codex` in your terminal to generate fresh tokens, " - "then run `hermes login --provider openai-codex` to re-authenticate." + "then run `hermes auth` to re-authenticate." ) relogin_required = True raise AuthError( @@ -1140,7 +1140,7 @@ def resolve_codex_runtime_credentials( logger.info("Migrating Codex credentials from ~/.codex/ to Hermes auth store") print("⚠️ Migrating Codex credentials to Hermes's own auth store.") print(" This avoids conflicts with Codex CLI and VS Code.") - print(" Run `hermes login` to create a fully independent session.\n") + print(" Run `hermes auth` to create a fully independent session.\n") _save_codex_tokens(cli_tokens) data = _read_codex_tokens() else: @@ -2096,7 +2096,7 @@ def detect_external_credentials() -> List[Dict[str, Any]]: found.append({ "provider": "openai-codex", "path": str(codex_path), - "label": f"Codex CLI credentials found ({codex_path}) — run `hermes login` to create a separate session", + "label": f"Codex CLI credentials found ({codex_path}) — run `hermes auth` to create a separate session", }) return found @@ -2345,8 +2345,8 @@ def _save_model_choice(model_id: str) -> None: def login_command(args) -> None: """Deprecated: use 'hermes model' or 'hermes setup' instead.""" print("The 'hermes login' command has been removed.") - print("Use 'hermes model' to select a provider and model,") - print("or 'hermes setup' for full interactive setup.") + print("Use 'hermes auth' to manage credentials,") + print("'hermes model' to select a provider, or 'hermes setup' for full setup.") raise SystemExit(0) diff --git a/hermes_cli/auth_commands.py b/hermes_cli/auth_commands.py index 1564c1000e..395dbb76c7 100644 --- a/hermes_cli/auth_commands.py +++ b/hermes_cli/auth_commands.py @@ -305,6 +305,32 @@ def auth_remove_command(args) -> None: if cleared: print(f"Cleared {env_var} from .env") + # If this was a singleton-seeded credential (OAuth device_code, hermes_pkce), + # clear the underlying auth store / credential file so it doesn't get + # re-seeded on the next load_pool() call. + elif removed.source == "device_code" and provider in ("openai-codex", "nous"): + from hermes_cli.auth import ( + _load_auth_store, _save_auth_store, _auth_store_lock, + ) + with _auth_store_lock(): + auth_store = _load_auth_store() + providers_dict = auth_store.get("providers") + if isinstance(providers_dict, dict) and provider in providers_dict: + del providers_dict[provider] + _save_auth_store(auth_store) + print(f"Cleared {provider} OAuth tokens from auth store") + + elif removed.source == "hermes_pkce" and provider == "anthropic": + from hermes_constants import get_hermes_home + oauth_file = get_hermes_home() / ".anthropic_oauth.json" + if oauth_file.exists(): + oauth_file.unlink() + print("Cleared Hermes Anthropic OAuth credentials") + + elif removed.source == "claude_code" and provider == "anthropic": + print("Note: Claude Code credentials live in ~/.claude/.credentials.json") + print(" Remove them manually if you want to deauthorize Claude Code.") + def auth_reset_command(args) -> None: provider = _normalize_provider(getattr(args, "provider", "")) diff --git a/hermes_cli/config.py b/hermes_cli/config.py index bf0b27c205..8863bda593 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -1932,8 +1932,8 @@ _FALLBACK_COMMENT = """ # # Supported providers: # openrouter (OPENROUTER_API_KEY) — routes to any model -# openai-codex (OAuth — hermes login) — OpenAI Codex -# nous (OAuth — hermes login) — Nous Portal +# openai-codex (OAuth — hermes auth) — OpenAI Codex +# nous (OAuth — hermes auth) — Nous Portal # zai (ZAI_API_KEY) — Z.AI / GLM # kimi-coding (KIMI_API_KEY) — Kimi / Moonshot # minimax (MINIMAX_API_KEY) — MiniMax @@ -1975,8 +1975,8 @@ _COMMENTED_SECTIONS = """ # # Supported providers: # openrouter (OPENROUTER_API_KEY) — routes to any model -# openai-codex (OAuth — hermes login) — OpenAI Codex -# nous (OAuth — hermes login) — Nous Portal +# openai-codex (OAuth — hermes auth) — OpenAI Codex +# nous (OAuth — hermes auth) — Nous Portal # zai (ZAI_API_KEY) — Z.AI / GLM # kimi-coding (KIMI_API_KEY) — Kimi / Moonshot # minimax (MINIMAX_API_KEY) — MiniMax diff --git a/run_agent.py b/run_agent.py index d97e08ad55..ffe94774e5 100644 --- a/run_agent.py +++ b/run_agent.py @@ -8191,7 +8191,7 @@ class AIAgent: self._vprint(f"{self.log_prefix} 💡 Codex OAuth token was rejected (HTTP 401). Your token may have been", force=True) self._vprint(f"{self.log_prefix} refreshed by another client (Codex CLI, VS Code). To fix:", force=True) self._vprint(f"{self.log_prefix} 1. Run `codex` in your terminal to generate fresh tokens.", force=True) - self._vprint(f"{self.log_prefix} 2. Then run `hermes login --provider openai-codex` to re-authenticate.", force=True) + self._vprint(f"{self.log_prefix} 2. Then run `hermes auth` to re-authenticate.", force=True) else: self._vprint(f"{self.log_prefix} 💡 Your API key was rejected by the provider. Check:", force=True) self._vprint(f"{self.log_prefix} • Is the key valid? Run: hermes setup", force=True) diff --git a/tools/delegate_tool.py b/tools/delegate_tool.py index 71a78ea664..28ffc795a9 100644 --- a/tools/delegate_tool.py +++ b/tools/delegate_tool.py @@ -701,7 +701,7 @@ def _resolve_delegation_credentials(cfg: dict, parent_agent) -> dict: if not api_key: raise ValueError( f"Delegation provider '{configured_provider}' resolved but has no API key. " - f"Set the appropriate environment variable or run 'hermes login'." + f"Set the appropriate environment variable or run 'hermes auth'." ) return { diff --git a/website/docs/reference/cli-commands.md b/website/docs/reference/cli-commands.md index b2df9bca8d..5fbe921b5e 100644 --- a/website/docs/reference/cli-commands.md +++ b/website/docs/reference/cli-commands.md @@ -37,8 +37,8 @@ hermes [global-options] [subcommand/options] | `hermes gateway` | Run or manage the messaging gateway service. | | `hermes setup` | Interactive setup wizard for all or part of the configuration. | | `hermes whatsapp` | Configure and pair the WhatsApp bridge. | -| `hermes login` / `logout` | Authenticate with OAuth-backed providers. | -| `hermes auth` | Manage credential pools — add, list, remove, reset, set strategy. | +| `hermes auth` | Manage credentials — add, list, remove, reset, set strategy. Handles OAuth flows for Codex/Nous/Anthropic. | +| `hermes login` / `logout` | **Deprecated** — use `hermes auth` instead. | | `hermes status` | Show agent, auth, and platform status. | | `hermes cron` | Inspect and tick the cron scheduler. | | `hermes webhook` | Manage dynamic webhook subscriptions for event-driven activation. | @@ -178,22 +178,11 @@ hermes whatsapp Runs the WhatsApp pairing/setup flow, including mode selection and QR-code pairing. -## `hermes login` / `hermes logout` +## `hermes login` / `hermes logout` *(Deprecated)* -```bash -hermes login [--provider nous|openai-codex] [--portal-url ...] [--inference-url ...] -hermes logout [--provider nous|openai-codex] -``` - -`login` supports: -- Nous Portal OAuth/device flow -- OpenAI Codex OAuth/device flow - -Useful options for `login`: -- `--no-browser` -- `--timeout ` -- `--ca-bundle ` -- `--insecure` +:::caution +`hermes login` has been removed. Use `hermes auth` to manage OAuth credentials, `hermes model` to select a provider, or `hermes setup` for full interactive setup. +::: ## `hermes auth` diff --git a/website/docs/user-guide/configuration.md b/website/docs/user-guide/configuration.md index 7148b423d3..063329084f 100644 --- a/website/docs/user-guide/configuration.md +++ b/website/docs/user-guide/configuration.md @@ -651,7 +651,7 @@ AUXILIARY_VISION_MODEL=openai/gpt-4o |----------|-------------|-------------| | `"auto"` | Best available (default). Vision tries OpenRouter → Nous → Codex. | — | | `"openrouter"` | Force OpenRouter — routes to any model (Gemini, GPT-4o, Claude, etc.) | `OPENROUTER_API_KEY` | -| `"nous"` | Force Nous Portal | `hermes login` | +| `"nous"` | Force Nous Portal | `hermes auth` | | `"codex"` | Force Codex OAuth (ChatGPT account). Supports vision (gpt-5.3-codex). | `hermes model` → Codex | | `"main"` | Use your active custom/main endpoint. This can come from `OPENAI_BASE_URL` + `OPENAI_API_KEY` or from a custom endpoint saved via `hermes model` / `config.yaml`. Works with OpenAI, local models, or any OpenAI-compatible API. | Custom endpoint credentials + base URL | diff --git a/website/docs/user-guide/features/fallback-providers.md b/website/docs/user-guide/features/fallback-providers.md index a5cdc5bac1..8868162e83 100644 --- a/website/docs/user-guide/features/fallback-providers.md +++ b/website/docs/user-guide/features/fallback-providers.md @@ -37,7 +37,7 @@ Both `provider` and `model` are **required**. If either is missing, the fallback |----------|-------|-------------| | AI Gateway | `ai-gateway` | `AI_GATEWAY_API_KEY` | | OpenRouter | `openrouter` | `OPENROUTER_API_KEY` | -| Nous Portal | `nous` | `hermes login` (OAuth) | +| Nous Portal | `nous` | `hermes auth` (OAuth) | | OpenAI Codex | `openai-codex` | `hermes model` (ChatGPT OAuth) | | GitHub Copilot | `copilot` | `COPILOT_GITHUB_TOKEN`, `GH_TOKEN`, or `GITHUB_TOKEN` | | GitHub Copilot ACP | `copilot-acp` | External process (editor integration) | @@ -244,7 +244,7 @@ All three — auxiliary, compression, fallback — work the same way: set `provi |----------|-------------|-------------| | `"auto"` | Try providers in order until one works (default) | At least one provider configured | | `"openrouter"` | Force OpenRouter | `OPENROUTER_API_KEY` | -| `"nous"` | Force Nous Portal | `hermes login` | +| `"nous"` | Force Nous Portal | `hermes auth` | | `"codex"` | Force Codex OAuth | `hermes model` → Codex | | `"main"` | Use whatever provider the main agent uses | Active main provider configured | | `"anthropic"` | Force Anthropic native | `ANTHROPIC_API_KEY` or Claude Code credentials |