diff --git a/docs/AUTH-ANALYSIS.md b/docs/AUTH-ANALYSIS.md new file mode 100644 index 0000000..2861767 --- /dev/null +++ b/docs/AUTH-ANALYSIS.md @@ -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: ` | +| `sk-ant-oat*` | OAuth/setup token | `Authorization: Bearer ` | +| (JWT/other) | Bearer auth | `Authorization: Bearer ` | + +**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.) diff --git a/module.nix b/module.nix index 64a2441..e06c987 100644 --- a/module.nix +++ b/module.nix @@ -166,9 +166,27 @@ in type = types.nullOr types.path; default = null; description = '' - Path to an auth.json file containing OAuth credentials (Nous Portal, Codex, Anthropic OAuth). - Use with sops-nix to manage OAuth tokens declaratively. - If null, auth.json is managed at runtime via `hermes model`. + Path to an auth.json seed file containing OAuth credentials + (Nous Portal, Codex, Anthropic OAuth). + + This file is only copied on first deploy — if auth.json already exists + in the state directory, it is NOT overwritten. This preserves runtime + token refreshes across rebuilds and restarts. + + To force a re-seed, delete the existing auth.json first: + rm /var/lib/hermes/.hermes/auth.json + + If null, auth.json is managed entirely at runtime via `hermes login`. + ''; + }; + + authFileForceOverwrite = mkOption { + type = types.bool; + default = false; + description = '' + If true, always overwrite auth.json from authFile on activation. + WARNING: This destroys any runtime-refreshed tokens. + Only use for testing or when you know the seed file has fresh tokens. ''; }; @@ -282,9 +300,15 @@ in # Link config file install -o ${cfg.user} -g ${cfg.group} -m 0640 -D ${configFile} ${cfg.stateDir}/.hermes/cli-config.yaml - # Link auth file if provided + # Seed auth file if provided (only if not already present, unless force overwrite) ${lib.optionalString (cfg.authFile != null) '' - install -o ${cfg.user} -g ${cfg.group} -m 0600 ${cfg.authFile} ${cfg.stateDir}/.hermes/auth.json + ${if cfg.authFileForceOverwrite then '' + install -o ${cfg.user} -g ${cfg.group} -m 0600 ${cfg.authFile} ${cfg.stateDir}/.hermes/auth.json + '' else '' + if [ ! -f ${cfg.stateDir}/.hermes/auth.json ]; then + install -o ${cfg.user} -g ${cfg.group} -m 0600 ${cfg.authFile} ${cfg.stateDir}/.hermes/auth.json + fi + ''} ''} # Link documents into workspace