PR Pipeline — Authorize → Validate → Review → Merge / auth (pull_request) Successful in 4s
PR Pipeline — Authorize → Validate → Review → Merge / validate (pull_request) Successful in 3s
PR Pipeline — Authorize → Validate → Review → Merge / lint (pull_request) Successful in 1s
PR Pipeline — Authorize → Validate → Review → Merge / ai-review (pull_request) Successful in 5s
PR Pipeline — Authorize → Validate → Review → Merge / gate (pull_request) Successful in 0s
Mumuni CT114 destroyed. Mumuni now runs inside Abiba CT100 at 192.168.68.24. Updated all contract files and agent-health-check.py.
302 lines
12 KiB
Markdown
302 lines
12 KiB
Markdown
---
|
|
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
|
|
|
|
> **Canonical vault process (2026-07-16):** see `litellm-api-keys` § Production Vault Access Process.
|
|
> All agents MUST use the `infisical-gateway.sh` wrapper (live vault injection). Hardcoded systemd
|
|
> drop-ins / config.yaml keys are DEPRECATED — they rot on rotation (root cause of the 2026-07-16 401 storm).
|
|
> 4/5 agents migrated; tanko (user jerome) pending.
|
|
|
|
```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@<host> "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 | 100 (abiba) | .24 | `mumuni` | Infisical vault | ✅ Fixed | Pi Hermes gateway | 2026-07-27 |
|
|
| 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-<agent-key-from-vault> # ← 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-<agent-key-from-vault> # ← 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
|