--- kind: enforcement name: hermes-key-enforcement version: 1.0.0 description: > Enforces standardized API key configuration across all Hermes agents. Harness/LiteLLM providers MUST use api_key_env indirection. External providers (DeepSeek, OpenAI, Anthropic) may use hardcoded keys. Single source of truth: Infisical vault (project=agents, env=production) — injected at runtime via `infisical run --` wrapper. /etc/environment is DEPRECATED for agent keys post-migration. Designed to make key rotation a one-step vault operation. author: Abiba (pi agent) --- # Hermes Key Enforcement Contract ## Rule (One Sentence) **All harness/litellm providers MUST use `api_key_env: LITELLM_API_KEY` with authenticated path `http://192.168.68.116/litellm/v1/responses` — hardcoded keys AND unauthenticated `/v1` direct access are both forbidden.** ## Scope Applies to all Hermes agent configs across all hosts. Covers these config sections: - `model.api_key` - `custom_providers[].api_key` (when `name` contains `harness` or `litellm`) - `auxiliary.*.api_key` (when `provider` is `harness` or contains `litellm`) - `delegation.api_key` (when `provider` is `harness` or contains `litellm`) - `compression.api_key` (when `provider` is `harness` or contains `litellm`) - `fallback_providers[].api_key` (when provider is harness) ## Architecture (2026-07-10) Syslog is migrating away from **unauthenticated direct access** to the shared inference harness. | Path | Auth | Status | |------|------|--------| | `http://192.168.68.116/v1` | None (direct) | ❌ **DEPRECATED** — being phased out | | `http://192.168.68.116/litellm/v1/responses` | Bearer `sk-*` key | ✅ **CURRENT** — authenticated LiteLLM proxy | All harness/litellm providers MUST use the authenticated `/litellm/v1/responses` path. Any `base_url` pointing to bare `/v1` on 192.168.68.116 is a **migration violation**. ### 🔥 CRITICAL: Double-Path Bug (2026-07-10) When `api_mode: responses` is set, Hermes **appends `/v1/responses`** to `base_url`. If `base_url` already includes `/litellm/v1/responses`, the result is: ``` http://192.168.68.116/litellm/v1/responses/v1/responses → 404 ``` **The `base_url` must end at `/v1` — never include `/responses`:** ```yaml # ✅ CORRECT — Hermes appends /v1/responses for api_mode: responses base_url: http://192.168.68.116/litellm/v1 # ❌ WRONG — produces double path base_url: http://192.168.68.116/litellm/v1/responses ``` This applies to ALL sections using the harness provider: `custom_providers`, `delegation`, `auxiliary.*`. ## Exemptions External providers are **explicitly exempt** and may use hardcoded keys: - DeepSeek (`api.deepseek.com`) - OpenAI (`api.openai.com`) - Anthropic (`api.anthropic.com`) - OpenRouter - Any provider whose base_url does NOT match `192.168.68.116` or `litellm.sysloggh.net` ## Standard Pattern ```yaml # ✅ CORRECT — all harness/litellm providers (authenticated path, NO /responses suffix) model: provider: harness base_url: http://192.168.68.116/litellm/v1 # ← Hermes appends /v1/responses api_key_env: LITELLM_API_KEY custom_providers: - name: harness api_mode: responses base_url: http://192.168.68.116/litellm/v1 # ← NO /responses suffix! api_key_env: LITELLM_API_KEY auxiliary: compression: provider: harness base_url: http://192.168.68.116/litellm/v1 # ← NO /responses suffix! api_key_env: LITELLM_API_KEY # ✅ ALSO CORRECT — external providers fallback_providers: - provider: deepseek base_url: https://api.deepseek.com api_key: sk-b7d9... # ← hardcoded OK (external) api_key_env: DEEPSEEK_API_KEY # ← also OK if set in environment (vault or /etc/environment) ``` ```yaml # ❌ FORBIDDEN — hardcoded key (top) OR unauthenticated path (bottom) model: provider: harness api_key: sk-Flc62smlegyMEaSo1ka8JA # ← RULE VIOLATION: hardcoded key model: provider: harness base_url: http://192.168.68.116/v1 # ← RULE VIOLATION: unauthenticated path api_key_env: LITELLM_API_KEY ``` ## Detection Query Run on any Hermes host to detect violations: ```bash # 1. Check config.yaml for hardcoded harness keys grep -rn 'api_key: sk-' /root/.hermes/ \ --include='config.yaml' \ | grep -v 'deepseek\|openai\|anthropic\|DEEPSEEK' # 1b. Check for double-path bug: base_url ending with /responses # (Hermes appends /v1/responses when api_mode=responses, so base_url must end at /v1) grep -rn 'litellm/v1/responses' /root/.hermes/config.yaml # ANY output here = WRONG. Must be 'litellm/v1' without /responses suffix. # 2. Check systemd drop-ins for master key leaks (2026-07-05: Tanko had this) grep -rn 'LITELLM_API_KEY' /root/.config/systemd/user/ 2>/dev/null grep -rn 'LITELLM_API_KEY=sk-litellm-7f96080d' /root/.config/systemd/ 2>/dev/null # 3. Verify running process env matches dedicated key cat /proc/$(cat /home/jerome/.hermes/gateway.pid | python3 -c "import sys,json; print(json.load(sys.stdin)['pid'])")/environ \ | tr '\0' '\n' | grep LITELLM_API_KEY ``` If any output from step 2 — **critical violation** (master key leaked). Fix immediately. ## Rotation Procedure With this standard enforced, key rotation is one vault update: ```bash # 1. Generate new key in LiteLLM: POST /key/generate with agent alias # 2. Update Infisical vault secret infisical secrets set LITELLM_API_KEY=sk-NEW_KEY \ --project=agents --env=production # 3. Restart agent gateway (key auto-injected via infisical run -- wrapper) ssh root@ "systemctl restart hermes-gateway" # 4. Verify curl -s -H "Authorization: Bearer sk-NEW_KEY" http://192.168.68.116/litellm/v1/models ``` **Done.** No config file changes needed. No /etc/environment edits needed. The agent picks up the new key via `infisical run --` at gateway startup. > **Post-migration note**: /etc/environment is NO LONGER the key source. > Strip all `LITELLM_API_KEY` lines from /etc/environment (comment out with `# [INFISICAL]`) > and let the `infisical run --` wrapper inject the key at runtime. ## Key Longevity Policy (2026-07-04) **Keys are permanent and use bare agent name aliases.** - **Duration**: `null` — keys never expire. This is enforced by `default_key_generate_params` in `litellm_config.yaml`. - **Alias convention**: bare agent name only (e.g., `tanko`, `mumuni`, `koby`, `koonimo`). No dates, no versions. The alias IS the identity. - **Rotation triggers**: compromise, personnel departure, or quarterly security hygiene. NOT calendar-driven. - **Max budget**: $100 per key (config default). ```yaml # In litellm_config.yaml — ensures all future keys inherit these defaults: litellm_settings: default_key_generate_params: models: ["syslog-auto", "qwen3.6-27B-code", "gemma-4-12b"] duration: null # ← permanent max_budget: 100 metadata: purpose: "agent-inference" ``` ## Verified Agents (2026-07-05 update) | Agent | CT | IP | LiteLLM Alias | Key Source | Status | Gateway Wrapper | Last Verified | |-------|-----|-----|---------------|------------|--------|-----------------|---------------| | Tanko | 112 | .122 | `tanko` | Infisical vault | ✅ Fixed | `infisical run` | 20:17 UTC Jul 5 | | Mumuni | 114 | .123 | `mumuni` | Infisical vault | ✅ Fixed | `infisical run` | 01:46 EDT Jul 10 | | Koby | 111 | ? | `koby` | Infisical vault | ✅ Fixed | `infisical run` | 23:30 UTC Jul 5 | | Koonimo | 113 | ? | `koonimo` | Infisical vault | ✅ Fixed | `infisical run` (migrated 2026-07-11) | 2026-07-11 | | Abiba | 100 | .65 | `abiba-pi` | Infisical vault | ✅ N/A (pi native) | — | 19:44 UTC Jul 5 | | Kagenz0 | 105 | ? | — | — | ❌ DOWN | — | 19:14 EDT Jul 4 | > **Note**: CT hostnames (tdunna, baggy) differ from agent identities (koby, koonimo). > LiteLLM key aliases use agent identity, not CT hostname. ### Migration Status: Authenticated Path | Agent | `/litellm/v1/responses` | Deprecated `/v1` | Status | |-------|--------------------------|--------------------|--------| | Mumuni | ✅ 5 sections | 0 | ✅ Authenticated | | Tanko | ⚠️ No SSH access | — | Needs check | | Koby | ⚠️ No route to host | — | Needs check | | Koonimo | ⚠️ Connection timed out | — | Needs check | ### Systemd Service Pattern (2026-07-11 — vault migration) All Hermes agents use systemd to manage their gateway. The gateway service is wrapped with `infisical run --` to inject secrets at runtime. **Correct pattern (post-migration):** ```ini # Service file wraps gateway with Infisical: [Service] ExecStart=/usr/bin/infisical run --project=agents --env=production -- \ /usr/bin/hermes gateway run # /etc/environment is CLEAN — no LITELLM_API_KEY present # (strip it and tag with # [INFISICAL] if present) ``` **Legacy pattern (deprecated — pre-migration only):** ```ini # DO NOT USE post-migration: EnvironmentFile=/etc/environment # This pattern was replaced by infisical run -- wrapper ``` **Rotation procedure** (one vault operation with this standard): 1. Generate new key in LiteLLM: `curl /key/generate` with agent alias 2. Update Infisical vault: `infisical secrets set LITELLM_API_KEY=sk-NEW --project=agents --env=production` 3. Restart: `systemctl restart hermes-gateway` (key auto-injected via wrapper) ## Violation Response 1. **Detect** — run detection query above 2. **Fix** — replace `api_key: sk-...` with `api_key_env: LITELLM_API_KEY` in all harness/litellm sections 3. **Verify** — `grep -c "api_key_env" config.yaml` should increase, hardcoded harness keys should be 0 4. **Restart** — gateway must restart to pick up env var 5. **Confirm** — test key against LiteLLM: `curl -H "Authorization: Bearer $KEY" .../v1/models` → 200 6. **Update** — bump the verified table above 7. **Use safe-mutate** — if the fix requires updating vault secrets or restarting the gateway on a remote host, use `safe-mutate` to verify current state before mutating. ## Related Contracts - `hermes-config-template.prose.md` — full configuration template - `litellm-health.prose.md` — LiteLLM stack health verification - `zulip-platform-verification.prose.md` — cross-platform agent verification - `litellm-api-keys.prose.md` — API key creation, rotation, and verification ## CI Pipeline (2026-07-04) All contract changes must pass the PR Pipeline before merge: ``` auth → validate → lint → ai-review → gate ``` - **Trigger**: push to master (abiba-bot only) or pull request - **Branch protection**: Only `abiba-bot` can push directly to master. All other users must use PRs. - **Status check**: `PR Pipeline — Authorize → Validate → Review → Merge` required before merge - **Runner**: `runner-ct110` (Gitea Actions v0.6.1) on CT 110 - **Config**: `.gitea/workflows/pr-pipeline.yaml` ## Known Bug: auxiliary_client ignores api_key_env (2026-07-05) **Bug**: `_resolve_task_provider_model()` in `agent/auxiliary_client.py` reads `api_key` from auxiliary task configs (vision, compression, etc.) but does NOT resolve `api_key_env`. The custom provider resolution path handles `api_key_env`, but auxiliary tasks take a different code path that ignores it. **Impact**: Vision analysis and compression calls fall through to the `"no-key-required"` placeholder, causing 401 errors on LiteLLM/harness (which require `sk-*` keys). **Workaround**: Set `api_key` directly alongside `api_key_env` in each auxiliary task config: ```yaml auxiliary: vision: api_key: sk- # ← workaround (get via: infisical secrets get LITELLM_API_KEY --project=agents --env=production --plain) api_key_env: LITELLM_API_KEY base_url: http://192.168.68.116/v1 model: gemma-4-12b provider: harness compression: api_key: sk- # ← workaround (same as above) api_key_env: LITELLM_API_KEY base_url: http://192.168.68.116/v1 model: gemma-4-12b provider: harness ``` **Affected agents**: All Hermes agents with harness/LiteLLM provider and `api_key_env` in auxiliary configs (all 4 Hermes agents patched 2026-07-05). **Source location**: `agent/auxiliary_client.py` line 5478