mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-03 17:27:37 +08:00
Compare commits
1 Commits
fix/plugin
...
hermes/her
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a65ee52c0f |
@@ -849,7 +849,7 @@ def _resolve_forced_provider(forced: str) -> Tuple[Optional[OpenAI], Optional[st
|
|||||||
if forced == "nous":
|
if forced == "nous":
|
||||||
client, model = _try_nous()
|
client, model = _try_nous()
|
||||||
if client is None:
|
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
|
return client, model
|
||||||
|
|
||||||
if forced == "codex":
|
if forced == "codex":
|
||||||
@@ -1119,7 +1119,7 @@ def resolve_provider_client(
|
|||||||
client, default = _try_nous()
|
client, default = _try_nous()
|
||||||
if client is None:
|
if client is None:
|
||||||
logger.warning("resolve_provider_client: nous requested "
|
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
|
return None, None
|
||||||
final_model = model or default
|
final_model = model or default
|
||||||
return (_to_async_client(client, final_model) if async_mode
|
return (_to_async_client(client, final_model) if async_mode
|
||||||
|
|||||||
@@ -936,7 +936,7 @@ def _read_codex_tokens(*, _lock: bool = True) -> Dict[str, Any]:
|
|||||||
state = _load_provider_state(auth_store, "openai-codex")
|
state = _load_provider_state(auth_store, "openai-codex")
|
||||||
if not state:
|
if not state:
|
||||||
raise AuthError(
|
raise AuthError(
|
||||||
"No Codex credentials stored. Run `hermes login` to authenticate.",
|
"No Codex credentials stored. Run `hermes auth` to authenticate.",
|
||||||
provider="openai-codex",
|
provider="openai-codex",
|
||||||
code="codex_auth_missing",
|
code="codex_auth_missing",
|
||||||
relogin_required=True,
|
relogin_required=True,
|
||||||
@@ -944,7 +944,7 @@ def _read_codex_tokens(*, _lock: bool = True) -> Dict[str, Any]:
|
|||||||
tokens = state.get("tokens")
|
tokens = state.get("tokens")
|
||||||
if not isinstance(tokens, dict):
|
if not isinstance(tokens, dict):
|
||||||
raise AuthError(
|
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",
|
provider="openai-codex",
|
||||||
code="codex_auth_invalid_shape",
|
code="codex_auth_invalid_shape",
|
||||||
relogin_required=True,
|
relogin_required=True,
|
||||||
@@ -953,14 +953,14 @@ def _read_codex_tokens(*, _lock: bool = True) -> Dict[str, Any]:
|
|||||||
refresh_token = tokens.get("refresh_token")
|
refresh_token = tokens.get("refresh_token")
|
||||||
if not isinstance(access_token, str) or not access_token.strip():
|
if not isinstance(access_token, str) or not access_token.strip():
|
||||||
raise AuthError(
|
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",
|
provider="openai-codex",
|
||||||
code="codex_auth_missing_access_token",
|
code="codex_auth_missing_access_token",
|
||||||
relogin_required=True,
|
relogin_required=True,
|
||||||
)
|
)
|
||||||
if not isinstance(refresh_token, str) or not refresh_token.strip():
|
if not isinstance(refresh_token, str) or not refresh_token.strip():
|
||||||
raise AuthError(
|
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",
|
provider="openai-codex",
|
||||||
code="codex_auth_missing_refresh_token",
|
code="codex_auth_missing_refresh_token",
|
||||||
relogin_required=True,
|
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.
|
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():
|
if not isinstance(refresh_token, str) or not refresh_token.strip():
|
||||||
raise AuthError(
|
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",
|
provider="openai-codex",
|
||||||
code="codex_auth_missing_refresh_token",
|
code="codex_auth_missing_refresh_token",
|
||||||
relogin_required=True,
|
relogin_required=True,
|
||||||
@@ -1035,7 +1035,7 @@ def refresh_codex_oauth_pure(
|
|||||||
"Codex refresh token was already consumed by another client "
|
"Codex refresh token was already consumed by another client "
|
||||||
"(e.g. Codex CLI or VS Code extension). "
|
"(e.g. Codex CLI or VS Code extension). "
|
||||||
"Run `codex` in your terminal to generate fresh tokens, "
|
"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
|
relogin_required = True
|
||||||
raise AuthError(
|
raise AuthError(
|
||||||
@@ -1140,7 +1140,7 @@ def resolve_codex_runtime_credentials(
|
|||||||
logger.info("Migrating Codex credentials from ~/.codex/ to Hermes auth store")
|
logger.info("Migrating Codex credentials from ~/.codex/ to Hermes auth store")
|
||||||
print("⚠️ Migrating Codex credentials to Hermes's own auth store.")
|
print("⚠️ Migrating Codex credentials to Hermes's own auth store.")
|
||||||
print(" This avoids conflicts with Codex CLI and VS Code.")
|
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)
|
_save_codex_tokens(cli_tokens)
|
||||||
data = _read_codex_tokens()
|
data = _read_codex_tokens()
|
||||||
else:
|
else:
|
||||||
@@ -2096,7 +2096,7 @@ def detect_external_credentials() -> List[Dict[str, Any]]:
|
|||||||
found.append({
|
found.append({
|
||||||
"provider": "openai-codex",
|
"provider": "openai-codex",
|
||||||
"path": str(codex_path),
|
"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
|
return found
|
||||||
@@ -2345,8 +2345,8 @@ def _save_model_choice(model_id: str) -> None:
|
|||||||
def login_command(args) -> None:
|
def login_command(args) -> None:
|
||||||
"""Deprecated: use 'hermes model' or 'hermes setup' instead."""
|
"""Deprecated: use 'hermes model' or 'hermes setup' instead."""
|
||||||
print("The 'hermes login' command has been removed.")
|
print("The 'hermes login' command has been removed.")
|
||||||
print("Use 'hermes model' to select a provider and model,")
|
print("Use 'hermes auth' to manage credentials,")
|
||||||
print("or 'hermes setup' for full interactive setup.")
|
print("'hermes model' to select a provider, or 'hermes setup' for full setup.")
|
||||||
raise SystemExit(0)
|
raise SystemExit(0)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -305,6 +305,32 @@ def auth_remove_command(args) -> None:
|
|||||||
if cleared:
|
if cleared:
|
||||||
print(f"Cleared {env_var} from .env")
|
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:
|
def auth_reset_command(args) -> None:
|
||||||
provider = _normalize_provider(getattr(args, "provider", ""))
|
provider = _normalize_provider(getattr(args, "provider", ""))
|
||||||
|
|||||||
@@ -1932,8 +1932,8 @@ _FALLBACK_COMMENT = """
|
|||||||
#
|
#
|
||||||
# Supported providers:
|
# Supported providers:
|
||||||
# openrouter (OPENROUTER_API_KEY) — routes to any model
|
# openrouter (OPENROUTER_API_KEY) — routes to any model
|
||||||
# openai-codex (OAuth — hermes login) — OpenAI Codex
|
# openai-codex (OAuth — hermes auth) — OpenAI Codex
|
||||||
# nous (OAuth — hermes login) — Nous Portal
|
# nous (OAuth — hermes auth) — Nous Portal
|
||||||
# zai (ZAI_API_KEY) — Z.AI / GLM
|
# zai (ZAI_API_KEY) — Z.AI / GLM
|
||||||
# kimi-coding (KIMI_API_KEY) — Kimi / Moonshot
|
# kimi-coding (KIMI_API_KEY) — Kimi / Moonshot
|
||||||
# minimax (MINIMAX_API_KEY) — MiniMax
|
# minimax (MINIMAX_API_KEY) — MiniMax
|
||||||
@@ -1975,8 +1975,8 @@ _COMMENTED_SECTIONS = """
|
|||||||
#
|
#
|
||||||
# Supported providers:
|
# Supported providers:
|
||||||
# openrouter (OPENROUTER_API_KEY) — routes to any model
|
# openrouter (OPENROUTER_API_KEY) — routes to any model
|
||||||
# openai-codex (OAuth — hermes login) — OpenAI Codex
|
# openai-codex (OAuth — hermes auth) — OpenAI Codex
|
||||||
# nous (OAuth — hermes login) — Nous Portal
|
# nous (OAuth — hermes auth) — Nous Portal
|
||||||
# zai (ZAI_API_KEY) — Z.AI / GLM
|
# zai (ZAI_API_KEY) — Z.AI / GLM
|
||||||
# kimi-coding (KIMI_API_KEY) — Kimi / Moonshot
|
# kimi-coding (KIMI_API_KEY) — Kimi / Moonshot
|
||||||
# minimax (MINIMAX_API_KEY) — MiniMax
|
# minimax (MINIMAX_API_KEY) — MiniMax
|
||||||
|
|||||||
@@ -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} 💡 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} 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} 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:
|
else:
|
||||||
self._vprint(f"{self.log_prefix} 💡 Your API key was rejected by the provider. Check:", force=True)
|
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)
|
self._vprint(f"{self.log_prefix} • Is the key valid? Run: hermes setup", force=True)
|
||||||
|
|||||||
@@ -701,7 +701,7 @@ def _resolve_delegation_credentials(cfg: dict, parent_agent) -> dict:
|
|||||||
if not api_key:
|
if not api_key:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Delegation provider '{configured_provider}' resolved but has no API key. "
|
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 {
|
return {
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ hermes [global-options] <command> [subcommand/options]
|
|||||||
| `hermes gateway` | Run or manage the messaging gateway service. |
|
| `hermes gateway` | Run or manage the messaging gateway service. |
|
||||||
| `hermes setup` | Interactive setup wizard for all or part of the configuration. |
|
| `hermes setup` | Interactive setup wizard for all or part of the configuration. |
|
||||||
| `hermes whatsapp` | Configure and pair the WhatsApp bridge. |
|
| `hermes whatsapp` | Configure and pair the WhatsApp bridge. |
|
||||||
| `hermes login` / `logout` | Authenticate with OAuth-backed providers. |
|
| `hermes auth` | Manage credentials — add, list, remove, reset, set strategy. Handles OAuth flows for Codex/Nous/Anthropic. |
|
||||||
| `hermes auth` | Manage credential pools — add, list, remove, reset, set strategy. |
|
| `hermes login` / `logout` | **Deprecated** — use `hermes auth` instead. |
|
||||||
| `hermes status` | Show agent, auth, and platform status. |
|
| `hermes status` | Show agent, auth, and platform status. |
|
||||||
| `hermes cron` | Inspect and tick the cron scheduler. |
|
| `hermes cron` | Inspect and tick the cron scheduler. |
|
||||||
| `hermes webhook` | Manage dynamic webhook subscriptions for event-driven activation. |
|
| `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.
|
Runs the WhatsApp pairing/setup flow, including mode selection and QR-code pairing.
|
||||||
|
|
||||||
## `hermes login` / `hermes logout`
|
## `hermes login` / `hermes logout` *(Deprecated)*
|
||||||
|
|
||||||
```bash
|
:::caution
|
||||||
hermes login [--provider nous|openai-codex] [--portal-url ...] [--inference-url ...]
|
`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 logout [--provider nous|openai-codex]
|
:::
|
||||||
```
|
|
||||||
|
|
||||||
`login` supports:
|
|
||||||
- Nous Portal OAuth/device flow
|
|
||||||
- OpenAI Codex OAuth/device flow
|
|
||||||
|
|
||||||
Useful options for `login`:
|
|
||||||
- `--no-browser`
|
|
||||||
- `--timeout <seconds>`
|
|
||||||
- `--ca-bundle <pem>`
|
|
||||||
- `--insecure`
|
|
||||||
|
|
||||||
## `hermes auth`
|
## `hermes auth`
|
||||||
|
|
||||||
|
|||||||
@@ -651,7 +651,7 @@ AUXILIARY_VISION_MODEL=openai/gpt-4o
|
|||||||
|----------|-------------|-------------|
|
|----------|-------------|-------------|
|
||||||
| `"auto"` | Best available (default). Vision tries OpenRouter → Nous → Codex. | — |
|
| `"auto"` | Best available (default). Vision tries OpenRouter → Nous → Codex. | — |
|
||||||
| `"openrouter"` | Force OpenRouter — routes to any model (Gemini, GPT-4o, Claude, etc.) | `OPENROUTER_API_KEY` |
|
| `"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 |
|
| `"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 |
|
| `"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 |
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ Both `provider` and `model` are **required**. If either is missing, the fallback
|
|||||||
|----------|-------|-------------|
|
|----------|-------|-------------|
|
||||||
| AI Gateway | `ai-gateway` | `AI_GATEWAY_API_KEY` |
|
| AI Gateway | `ai-gateway` | `AI_GATEWAY_API_KEY` |
|
||||||
| OpenRouter | `openrouter` | `OPENROUTER_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) |
|
| OpenAI Codex | `openai-codex` | `hermes model` (ChatGPT OAuth) |
|
||||||
| GitHub Copilot | `copilot` | `COPILOT_GITHUB_TOKEN`, `GH_TOKEN`, or `GITHUB_TOKEN` |
|
| GitHub Copilot | `copilot` | `COPILOT_GITHUB_TOKEN`, `GH_TOKEN`, or `GITHUB_TOKEN` |
|
||||||
| GitHub Copilot ACP | `copilot-acp` | External process (editor integration) |
|
| 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 |
|
| `"auto"` | Try providers in order until one works (default) | At least one provider configured |
|
||||||
| `"openrouter"` | Force OpenRouter | `OPENROUTER_API_KEY` |
|
| `"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 |
|
| `"codex"` | Force Codex OAuth | `hermes model` → Codex |
|
||||||
| `"main"` | Use whatever provider the main agent uses | Active main provider configured |
|
| `"main"` | Use whatever provider the main agent uses | Active main provider configured |
|
||||||
| `"anthropic"` | Force Anthropic native | `ANTHROPIC_API_KEY` or Claude Code credentials |
|
| `"anthropic"` | Force Anthropic native | `ANTHROPIC_API_KEY` or Claude Code credentials |
|
||||||
|
|||||||
Reference in New Issue
Block a user