mirror of
https://github.com/0xrsydn/nix-hermes-agent.git
synced 2026-08-07 00:53:52 +00:00
feat: persistent auth — seed-once, never overwrite on rebuild
- authFile now only copies to auth.json if it doesn't already exist - Runtime token refreshes (OAuth rotate) survive nixos-rebuild - Added authFileForceOverwrite option (default false) as escape hatch - Committed AUTH-ANALYSIS.md documenting full auth architecture
This commit is contained in:
parent
6024741356
commit
e7b70a7b4d
2 changed files with 679 additions and 5 deletions
650
docs/AUTH-ANALYSIS.md
Normal file
650
docs/AUTH-ANALYSIS.md
Normal file
|
|
@ -0,0 +1,650 @@
|
|||
# Hermes Agent Authentication System Analysis
|
||||
|
||||
**Date:** 2026-03-16
|
||||
**Source:** `/nix/store/3nz25qic608jccip6a6m49da24dxibi8-hermes-agent-0.2.0`
|
||||
**Version:** 0.2.0
|
||||
|
||||
---
|
||||
|
||||
## 1. Auth Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ HERMES AUTH RESOLUTION FLOW │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌──────────────┐
|
||||
│ CLI Call │
|
||||
│ hermes chat │
|
||||
└──────┬───────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────┐
|
||||
│ resolve_runtime_provider() │
|
||||
│ (runtime_provider.py) │
|
||||
└────────────────┬───────────────┘
|
||||
│
|
||||
┌────────────────┴────────────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
┌───────────────────────┐ ┌───────────────────────┐
|
||||
│ Config-based provider │ │ resolve_provider() │
|
||||
│ (config.yaml) │ │ (auth.py) │
|
||||
└───────────┬───────────┘ └───────────┬───────────┘
|
||||
│ │
|
||||
│ ┌───────────────────────┼───────────────────────┐
|
||||
│ │ │ │
|
||||
▼ ▼ ▼ ▼
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ PROVIDER RESOLUTION PRIORITY │
|
||||
├─────────────────────────────────────────────────────────────────────────┤
|
||||
│ 1. explicit --api-key / --base-url CLI args → openrouter │
|
||||
│ 2. active_provider in ~/.hermes/auth.json (OAuth providers) │
|
||||
│ 3. OPENAI_API_KEY / OPENROUTER_API_KEY env vars → openrouter │
|
||||
│ 4. Provider-specific env vars (GLM_API_KEY, KIMI_API_KEY, etc.) │
|
||||
│ 5. Fallback → openrouter │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────────────────┼───────────────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ OAUTH PROVIDERS │ │ API-KEY PROVIDERS│ │ OPENROUTER │
|
||||
│ (auth.json) │ │ (env vars) │ │ (default) │
|
||||
├─────────────────┤ ├─────────────────┤ ├─────────────────┤
|
||||
│ • nous │ │ • zai (GLM) │ │ OPENROUTER_API_ │
|
||||
│ • openai-codex │ │ • kimi-coding │ │ KEY or │
|
||||
│ │ │ • minimax │ │ OPENAI_API_KEY │
|
||||
│ Device Code → │ │ • minimax-cn │ │ │
|
||||
│ access_token │ │ • anthropic │ │ OpenRouter API │
|
||||
│ + refresh_token │ │ │ │ or custom URL │
|
||||
│ + agent_key │ │ Direct env vars │ │ via OPENAI_BASE │
|
||||
│ │ │ → runtime creds │ │ _URL │
|
||||
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
|
||||
│ │ │
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ RUNTIME CREDENTIALS │
|
||||
│ { │
|
||||
│ "provider": "nous" | "openai-codex" | "zai" | ... | "openrouter", │
|
||||
│ "api_mode": "chat_completions" | "anthropic_messages" | "codex_responses",│
|
||||
│ "base_url": "https://...", │
|
||||
│ "api_key": "...", │
|
||||
│ "source": "portal" | "env" | "hermes-auth-store", │
|
||||
│ "expires_at": "ISO timestamp" (OAuth only), │
|
||||
│ } │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. auth.json Schema
|
||||
|
||||
**Location:** `~/.hermes/auth.json`
|
||||
**Version:** 1
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"active_provider": "nous" | "openai-codex" | null,
|
||||
"providers": {
|
||||
"nous": {
|
||||
// OAuth device code flow tokens
|
||||
"portal_base_url": "https://portal.nousresearch.com",
|
||||
"inference_base_url": "https://inference-api.nousresearch.com/v1",
|
||||
"client_id": "hermes-cli",
|
||||
"scope": "inference:mint_agent_key",
|
||||
"token_type": "Bearer",
|
||||
|
||||
// Core OAuth tokens
|
||||
"access_token": "eyJ...",
|
||||
"refresh_token": "rt_...",
|
||||
"obtained_at": "2026-03-16T00:00:00+00:00",
|
||||
"expires_at": "2026-03-16T01:00:00+00:00",
|
||||
"expires_in": 3600,
|
||||
|
||||
// Minted agent key (short-lived inference key)
|
||||
"agent_key": "ak_...",
|
||||
"agent_key_id": "key-uuid",
|
||||
"agent_key_expires_at": "2026-03-16T00:30:00+00:00",
|
||||
"agent_key_expires_in": 1800,
|
||||
"agent_key_reused": false,
|
||||
"agent_key_obtained_at": "2026-03-16T00:00:00+00:00",
|
||||
|
||||
// TLS config
|
||||
"tls": {
|
||||
"insecure": false,
|
||||
"ca_bundle": null
|
||||
}
|
||||
},
|
||||
|
||||
"openai-codex": {
|
||||
// Codex OAuth tokens (stored in Hermes, not ~/.codex/)
|
||||
"tokens": {
|
||||
"access_token": "eyJ...",
|
||||
"refresh_token": "rt_..."
|
||||
},
|
||||
"last_refresh": "2026-03-16T00:00:00Z",
|
||||
"auth_mode": "chatgpt"
|
||||
}
|
||||
},
|
||||
"updated_at": "2026-03-16T00:00:00+00:00"
|
||||
}
|
||||
```
|
||||
|
||||
### Key Points:
|
||||
|
||||
- **Cross-process locking**: Uses file-based advisory lock (`auth.json.lock`) with 15s timeout
|
||||
- **Atomic writes**: Writes to temp file, then `os.replace()` for crash safety
|
||||
- **Permission restricted**: `chmod 0600` (owner read/write only)
|
||||
- **Version field**: Schema version for future migrations
|
||||
|
||||
---
|
||||
|
||||
## 3. Per-Provider Auth Flow
|
||||
|
||||
### 3.1 Nous Portal (OAuth Device Code)
|
||||
|
||||
**Provider ID:** `nous`
|
||||
**Auth Type:** `oauth_device_code`
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ NOUS PORTAL OAUTH FLOW │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
User runs: hermes login --provider nous
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────┐
|
||||
│ 1. POST /api/oauth/device/code │
|
||||
│ → device_code, user_code, │
|
||||
│ verification_uri_complete │
|
||||
└───────────────────┬────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────┐
|
||||
│ 2. User opens URL, enters code │
|
||||
│ Browser → Portal login → Authorize │
|
||||
└───────────────────┬────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────┐
|
||||
│ 3. Poll /api/oauth/token │
|
||||
│ (device_code grant) │
|
||||
│ → access_token, refresh_token │
|
||||
└───────────────────┬────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────┐
|
||||
│ 4. Mint agent key │
|
||||
│ POST /api/oauth/agent-key │
|
||||
│ Authorization: Bearer access_token │
|
||||
│ → api_key (short-lived, 30min TTL) │
|
||||
└───────────────────┬────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────┐
|
||||
│ 5. Store to ~/.hermes/auth.json │
|
||||
│ Set active_provider = "nous" │
|
||||
└────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Token Refresh:**
|
||||
- Access token auto-refreshed when `expires_at < now + 120s`
|
||||
- Agent key auto-re-minted when `agent_key_expires_at < now + 30min`
|
||||
- Refresh uses `refresh_token` grant to `/api/oauth/token`
|
||||
- If refresh fails with `invalid_grant`, user must re-login
|
||||
|
||||
**Code References:**
|
||||
- `auth.py:1413-1505` — `_request_device_code()`, `_poll_for_token()`
|
||||
- `auth.py:1507-1560` — `_refresh_access_token()`, `_mint_agent_key()`
|
||||
- `auth.py:1625-1780` — `resolve_nous_runtime_credentials()`
|
||||
|
||||
---
|
||||
|
||||
### 3.2 OpenAI Codex (OAuth External)
|
||||
|
||||
**Provider ID:** `openai-codex`
|
||||
**Auth Type:** `oauth_external`
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ CODEX OAUTH FLOW │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
User runs: hermes login --provider openai-codex
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────┐
|
||||
│ 1. POST /api/accounts/deviceauth/usercode│
|
||||
│ client_id: app_EMoamEEZ73f0CkXaXp7hrann│
|
||||
│ → user_code, device_auth_id │
|
||||
└───────────────────┬────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────┐
|
||||
│ 2. User opens auth.openai.com/codex/device│
|
||||
│ Enters user_code │
|
||||
└───────────────────┬────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────┐
|
||||
│ 3. Poll /api/accounts/deviceauth/token │
|
||||
│ → authorization_code, code_verifier │
|
||||
└───────────────────┬────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────┐
|
||||
│ 4. Exchange for tokens │
|
||||
│ POST /oauth/token │
|
||||
│ → access_token (JWT), refresh_token │
|
||||
└───────────────────┬────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────┐
|
||||
│ 5. Store to ~/.hermes/auth.json │
|
||||
│ (NOT ~/.codex/auth.json) │
|
||||
│ Hermes maintains its own session │
|
||||
└────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Important Notes:**
|
||||
- Hermes stores Codex tokens in its **own** auth store, not `~/.codex/`
|
||||
- This avoids token rotation conflicts with Codex CLI / VS Code
|
||||
- On first run, Hermes can **migrate** tokens from `~/.codex/auth.json`
|
||||
- Access token is a JWT; expiry checked via `exp` claim
|
||||
|
||||
**Code References:**
|
||||
- `auth.py:840-1010` — `_codex_device_code_login()`, `_refresh_codex_auth_tokens()`
|
||||
- `auth.py:1015-1085` — `resolve_codex_runtime_credentials()`
|
||||
- `codex_models.py` — Model discovery from Codex API
|
||||
|
||||
---
|
||||
|
||||
### 3.3 Z.AI / GLM (API Key)
|
||||
|
||||
**Provider ID:** `zai`
|
||||
**Auth Type:** `api_key`
|
||||
|
||||
**Env Vars (checked in order):**
|
||||
1. `GLM_API_KEY`
|
||||
2. `ZAI_API_KEY`
|
||||
3. `Z_AI_API_KEY`
|
||||
|
||||
**Base URL Override:** `GLM_BASE_URL`
|
||||
|
||||
**Endpoint Auto-Detection:**
|
||||
|
||||
Hermes probes multiple Z.AI endpoints to find one that accepts the API key:
|
||||
|
||||
| Endpoint ID | Base URL | Default Model |
|
||||
|------------------|---------------------------------------------------|---------------|
|
||||
| `global` | `https://api.z.ai/api/paas/v4` | `glm-5` |
|
||||
| `cn` | `https://open.bigmodel.cn/api/paas/v4` | `glm-5` |
|
||||
| `coding-global` | `https://api.z.ai/api/coding/paas/v4` | `glm-4.7` |
|
||||
| `coding-cn` | `https://open.bigmodel.cn/api/coding/paas/v4` | `glm-4.7` |
|
||||
|
||||
**Code References:**
|
||||
- `auth.py:350-390` — `detect_zai_endpoint()`
|
||||
- `auth.py:160-175` — Provider registry entry
|
||||
|
||||
---
|
||||
|
||||
### 3.4 Kimi / Moonshot (API Key)
|
||||
|
||||
**Provider ID:** `kimi-coding`
|
||||
**Auth Type:** `api_key`
|
||||
|
||||
**Env Var:** `KIMI_API_KEY`
|
||||
|
||||
**Base URL Auto-Detection:**
|
||||
|
||||
| Key Prefix | Base URL |
|
||||
|---------------|----------------------------------|
|
||||
| `sk-kimi-` | `https://api.kimi.com/coding/v1` |
|
||||
| (default) | `https://api.moonshot.ai/v1` |
|
||||
|
||||
**Override:** `KIMI_BASE_URL`
|
||||
|
||||
**Code References:**
|
||||
- `auth.py:305-325` — `_resolve_kimi_base_url()`
|
||||
- `auth.py:176-183` — Provider registry entry
|
||||
|
||||
---
|
||||
|
||||
### 3.5 MiniMax (API Key)
|
||||
|
||||
**Provider ID:** `minimax` (international) or `minimax-cn` (China)
|
||||
**Auth Type:** `api_key`
|
||||
|
||||
| Provider | Env Var | Base URL |
|
||||
|---------------|--------------------|---------------------------------|
|
||||
| `minimax` | `MINIMAX_API_KEY` | `https://api.minimax.io/v1` |
|
||||
| `minimax-cn` | `MINIMAX_CN_API_KEY`| `https://api.minimaxi.com/v1` |
|
||||
|
||||
**Override:** `MINIMAX_BASE_URL` / `MINIMAX_CN_BASE_URL`
|
||||
|
||||
**Code References:**
|
||||
- `auth.py:184-197` — Provider registry entries
|
||||
|
||||
---
|
||||
|
||||
### 3.6 Anthropic (API Key / OAuth)
|
||||
|
||||
**Provider ID:** `anthropic`
|
||||
**Auth Type:** `api_key` (with OAuth support)
|
||||
|
||||
**Resolution Priority:**
|
||||
|
||||
```
|
||||
1. ANTHROPIC_TOKEN env var
|
||||
2. CLAUDE_CODE_OAUTH_TOKEN env var
|
||||
3. ~/.claude/.credentials.json (claudeAiOauth.accessToken)
|
||||
└── Auto-refresh if expired + refreshToken available
|
||||
4. ANTHROPIC_API_KEY env var
|
||||
```
|
||||
|
||||
**Token Types:**
|
||||
|
||||
| Prefix | Auth Method | Headers |
|
||||
|-----------------|-----------------------|----------------------------------------|
|
||||
| `sk-ant-api*` | API key (x-api-key) | `x-api-key: <token>` |
|
||||
| `sk-ant-oat*` | OAuth/setup token | `Authorization: Bearer <token>` |
|
||||
| (JWT/other) | Bearer auth | `Authorization: Bearer <token>` |
|
||||
|
||||
**Beta Headers:**
|
||||
- All requests: `interleaved-thinking-2025-05-14`, `fine-grained-tool-streaming-2025-05-14`
|
||||
- OAuth only: `claude-code-20250219`, `oauth-2025-04-20`
|
||||
|
||||
**Code References:**
|
||||
- `agent/anthropic_adapter.py:1-100` — Token type detection, client building
|
||||
- `agent/anthropic_adapter.py:101-260` — Claude Code credential resolution, refresh
|
||||
- `agent/anthropic_adapter.py:261-310` — `resolve_anthropic_token()` priority chain
|
||||
|
||||
---
|
||||
|
||||
### 3.7 OpenRouter / Custom (API Key)
|
||||
|
||||
**Provider ID:** `openrouter` or `custom`
|
||||
**Auth Type:** `api_key` (fallback)
|
||||
|
||||
**Env Vars:**
|
||||
- `OPENROUTER_API_KEY` (preferred for OpenRouter)
|
||||
- `OPENAI_API_KEY` (fallback, or for custom endpoints)
|
||||
|
||||
**Base URL:**
|
||||
- Default: `https://openrouter.ai/api/v1`
|
||||
- Override: `OPENAI_BASE_URL` or `OPENROUTER_BASE_URL`
|
||||
|
||||
**Smart Key Selection:**
|
||||
- If URL contains `openrouter.ai` → prefer `OPENROUTER_API_KEY`
|
||||
- If custom URL → prefer `OPENAI_API_KEY`
|
||||
|
||||
**Custom Providers:**
|
||||
|
||||
Users can define custom providers in `config.yaml`:
|
||||
|
||||
```yaml
|
||||
custom_providers:
|
||||
- name: "local-llm"
|
||||
base_url: "http://localhost:11434/v1"
|
||||
api_key: "" # optional
|
||||
```
|
||||
|
||||
Then use with: `hermes chat --provider custom:local-llm`
|
||||
|
||||
**Code References:**
|
||||
- `runtime_provider.py:55-115` — `_resolve_openrouter_runtime()`, `_resolve_named_custom_runtime()`
|
||||
- `auth.py:775-830` — `resolve_provider()` priority chain
|
||||
|
||||
---
|
||||
|
||||
## 4. Token Lifecycle
|
||||
|
||||
### 4.1 Storage Locations
|
||||
|
||||
| Secret Type | Location | Permissions |
|
||||
|-----------------------|-------------------------------|-------------|
|
||||
| OAuth tokens | `~/.hermes/auth.json` | `0600` |
|
||||
| API keys | `~/.hermes/.env` | `0600` |
|
||||
| Claude Code OAuth | `~/.claude/.credentials.json` | `0600` |
|
||||
| Codex CLI OAuth | `~/.codex/auth.json` | (external) |
|
||||
|
||||
### 4.2 Token Refresh
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ TOKEN REFRESH MECHANISMS │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Provider │ Refresh Mechanism │ Trigger
|
||||
─────────────────┼────────────────────────────────┼─────────────────────
|
||||
Nous Portal │ refresh_token grant │ expires_at < now+2m
|
||||
│ → new access_token │
|
||||
│ → may rotate refresh_token │
|
||||
─────────────────┼────────────────────────────────┼─────────────────────
|
||||
Nous Agent Key │ Re-mint via agent-key endpoint │ expires_at < now+30m
|
||||
│ (uses valid access_token) │
|
||||
─────────────────┼────────────────────────────────┼─────────────────────
|
||||
Codex │ refresh_token grant │ JWT exp < now+2m
|
||||
│ → new access_token (JWT) │
|
||||
│ → may rotate refresh_token │
|
||||
─────────────────┼────────────────────────────────┼─────────────────────
|
||||
Anthropic OAuth │ Claude Code refresh endpoint │ expiresAt < now
|
||||
│ (only if refreshToken exists) │
|
||||
─────────────────┼────────────────────────────────┼─────────────────────
|
||||
API Key providers│ None (static keys) │ N/A
|
||||
```
|
||||
|
||||
### 4.3 Expiry Handling
|
||||
|
||||
**Nous Portal:**
|
||||
- Access token: ~1 hour TTL, refresh 2 minutes before expiry
|
||||
- Agent key: 30+ minute TTL, re-mint 30 minutes before expiry
|
||||
- On refresh failure with `invalid_grant`: `relogin_required=True`
|
||||
|
||||
**Codex:**
|
||||
- Access token: JWT with `exp` claim, refresh 2 minutes before expiry
|
||||
- On refresh failure with `invalid_grant`/`invalid_token`: `relogin_required=True`
|
||||
|
||||
**Anthropic (Claude Code):**
|
||||
- `expiresAt` in milliseconds since epoch
|
||||
- Refresh via `https://console.anthropic.com/v1/oauth/token`
|
||||
- Client ID: `9d1c250a-e61b-44d9-88ed-5944d1962f5e`
|
||||
|
||||
### 4.4 Error Handling
|
||||
|
||||
```python
|
||||
class AuthError(RuntimeError):
|
||||
message: str
|
||||
provider: str
|
||||
code: Optional[str] # "invalid_grant", "subscription_required", etc.
|
||||
relogin_required: bool
|
||||
```
|
||||
|
||||
**Common Error Codes:**
|
||||
|
||||
| Code | Meaning | Action |
|
||||
|--------------------------|---------------------------------|---------------------|
|
||||
| `invalid_grant` | Refresh token expired/revoked | Re-login required |
|
||||
| `invalid_token` | Access token invalid | Refresh or re-login |
|
||||
| `subscription_required` | No Nous subscription | User action needed |
|
||||
| `insufficient_credits` | Credits exhausted | User action needed |
|
||||
| `temporarily_unavailable`| Rate limited | Retry later |
|
||||
|
||||
---
|
||||
|
||||
## 5. Code References
|
||||
|
||||
### Core Auth Files
|
||||
|
||||
| File | Purpose | Key Functions |
|
||||
|-----------------------------------|-----------------------------------------------|---------------|
|
||||
| `hermes_cli/auth.py` | Main auth system (79KB) | `resolve_provider()`, `resolve_nous_runtime_credentials()`, `resolve_codex_runtime_credentials()`, `login_command()`, `logout_command()` |
|
||||
| `hermes_cli/runtime_provider.py` | Runtime credential resolution | `resolve_runtime_provider()`, `_resolve_openrouter_runtime()` |
|
||||
| `hermes_cli/config.py` | Config + env management | `save_env_value()`, `get_env_value()`, `load_config()` |
|
||||
| `hermes_cli/models.py` | Model catalogs + provider:model parsing | `parse_model_input()`, `provider_model_ids()` |
|
||||
| `hermes_cli/main.py` | CLI entry point | `_model_flow_nous()`, `_model_flow_codex()`, `_model_flow_api_key_provider()` |
|
||||
| `agent/anthropic_adapter.py` | Anthropic-specific auth + API adapter | `resolve_anthropic_token()`, `build_anthropic_client()` |
|
||||
| `acp_adapter/auth.py` | ACP server provider detection | `detect_provider()` |
|
||||
|
||||
### Key Functions by Line
|
||||
|
||||
**auth.py:**
|
||||
```
|
||||
L100-200 ProviderConfig dataclass, PROVIDER_REGISTRY
|
||||
L265-350 Auth store persistence (_load_auth_store, _save_auth_store)
|
||||
L380-420 Provider resolution (resolve_provider)
|
||||
L500-600 OAuth device code flow (_request_device_code, _poll_for_token)
|
||||
L605-700 Nous token refresh + agent key minting
|
||||
L715-780 Nous runtime credential resolution
|
||||
L840-1010 Codex OAuth flow + token refresh
|
||||
L1015-1085 Codex runtime credential resolution
|
||||
L1090-1160 API key provider resolution
|
||||
L1170-1260 Status helpers (get_auth_status)
|
||||
L1340-1500 Login commands (_login_nous, _login_openai_codex)
|
||||
L1625-1780 resolve_nous_runtime_credentials
|
||||
```
|
||||
|
||||
**runtime_provider.py:**
|
||||
```
|
||||
L25-55 resolve_requested_provider()
|
||||
L58-100 Custom provider resolution
|
||||
L105-145 OpenRouter/custom runtime resolution
|
||||
L150-230 resolve_runtime_provider() (main entry)
|
||||
```
|
||||
|
||||
**anthropic_adapter.py:**
|
||||
```
|
||||
L35-80 Token type detection (_is_oauth_token)
|
||||
L85-120 Claude Code credential reading
|
||||
L125-180 Token refresh logic
|
||||
L185-230 resolve_anthropic_token() priority chain
|
||||
L265-340 OAuth setup-token flow
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Implications for Nix
|
||||
|
||||
### 6.1 What Can Be Declarative
|
||||
|
||||
| Component | Declarative? | Notes |
|
||||
|--------------------|--------------|------------------------------------------------|
|
||||
| Config structure | ✅ Yes | `config.yaml` can be generated |
|
||||
| Default model | ✅ Yes | Set in config.yaml |
|
||||
| Toolsets | ✅ Yes | List in config.yaml |
|
||||
| Custom providers | ✅ Yes | Define in config.yaml |
|
||||
| Display settings | ✅ Yes | All in config.yaml |
|
||||
| Terminal backend | ✅ Yes | local/docker/singularity/ssh |
|
||||
|
||||
### 6.2 What Requires Runtime Interaction
|
||||
|
||||
| Component | Interactive? | Why |
|
||||
|--------------------------|--------------|----------------------------------------------|
|
||||
| Nous Portal OAuth | ✅ Yes | Requires browser-based device code auth |
|
||||
| Codex OAuth | ✅ Yes | Requires browser-based device code auth |
|
||||
| Anthropic OAuth | ✅ Yes | Requires `claude setup-token` or browser |
|
||||
| API Keys (first-time) | ⚠️ Semi | Can be env vars, but setup wizard helps |
|
||||
| Model selection | ⚠️ Semi | Can be declarative, but wizard discovers live models |
|
||||
|
||||
### 6.3 Nix Module Design Recommendations
|
||||
|
||||
```nix
|
||||
# Example NixOS module options
|
||||
services.hermes = {
|
||||
enable = true;
|
||||
|
||||
# Declarative config
|
||||
config = {
|
||||
model = {
|
||||
default = "anthropic/claude-opus-4.6";
|
||||
provider = "auto"; # or "nous", "openrouter", "zai", etc.
|
||||
};
|
||||
|
||||
terminal = {
|
||||
backend = "local";
|
||||
timeout = 180;
|
||||
};
|
||||
|
||||
display = {
|
||||
compact = true;
|
||||
personality = "kawaii";
|
||||
};
|
||||
};
|
||||
|
||||
# Environment variables (secrets)
|
||||
environmentFile = "/run/secrets/hermes.env";
|
||||
# OR individual secrets via sops-nix, agenix, etc.
|
||||
|
||||
# Custom providers
|
||||
customProviders = [
|
||||
{
|
||||
name = "local-vllm";
|
||||
baseUrl = "http://localhost:8000/v1";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
# Secrets file format (hermes.env):
|
||||
# OPENROUTER_API_KEY=sk-or-...
|
||||
# GLM_API_KEY=...
|
||||
# ANTHROPIC_API_KEY=sk-ant-api-...
|
||||
```
|
||||
|
||||
### 6.4 Auth State Management
|
||||
|
||||
**For Nix deployments:**
|
||||
|
||||
1. **API Key Providers** (recommended for servers):
|
||||
- Use `environmentFile` with secrets management
|
||||
- Keys: `OPENROUTER_API_KEY`, `GLM_API_KEY`, `ANTHROPIC_API_KEY`, etc.
|
||||
- No interactive auth required
|
||||
|
||||
2. **OAuth Providers** (not recommended for headless):
|
||||
- `auth.json` must be provisioned after first login
|
||||
- Could use `systemd-tmpfiles` to pre-seed, but tokens expire
|
||||
- Better: use API key providers for automated setups
|
||||
|
||||
3. **Anthropic via Claude Code**:
|
||||
- Can use `~/.claude/.credentials.json` if pre-provisioned
|
||||
- Or set `ANTHROPIC_API_KEY` for API key mode
|
||||
|
||||
### 6.5 File Locations for Nix
|
||||
|
||||
```nix
|
||||
# Hermes home directory
|
||||
environment.variables.HERMES_HOME = "/var/lib/hermes";
|
||||
|
||||
# Or per-user
|
||||
users.users.myuser.home = "/home/myuser";
|
||||
# HERMES_HOME defaults to ~/.hermes
|
||||
```
|
||||
|
||||
**Required directories:**
|
||||
- `$HERMES_HOME/` (root)
|
||||
- `$HERMES_HOME/cron/`
|
||||
- `$HERMES_HOME/sessions/`
|
||||
- `$HERMES_HOME/logs/`
|
||||
- `$HERMES_HOME/memories/`
|
||||
|
||||
---
|
||||
|
||||
## 7. Summary
|
||||
|
||||
Hermes uses a sophisticated multi-provider auth system with:
|
||||
|
||||
1. **OAuth Device Code Flow** for Nous Portal and Codex — interactive browser-based auth with automatic token refresh
|
||||
2. **API Key Resolution** for OpenRouter, Z.AI, Kimi, MiniMax, Anthropic — environment variable based with smart fallbacks
|
||||
3. **Unified Credential Resolution** via `resolve_runtime_provider()` — single entry point for all providers
|
||||
4. **Cross-Process Safety** with file locking and atomic writes to `auth.json`
|
||||
5. **Automatic Token Refresh** for OAuth providers with configurable skew
|
||||
6. **Graceful Error Handling** with user-friendly messages and `relogin_required` hints
|
||||
|
||||
For Nix packaging:
|
||||
- API key providers are fully declarative
|
||||
- OAuth providers require one-time interactive login
|
||||
- Config can be fully declarative via YAML
|
||||
- Secrets should use Nix secrets management (sops-nix, agenix, etc.)
|
||||
Loading…
Add table
Add a link
Reference in a new issue