The helper was correct but dead code - nothing called it. This commit: 1. Makes the helper runnable standalone: scripts/hermes-reachability-check.sh <host> <pattern> <path> 2. Wires all THREE contracts to it: - hermes-key-enforcement.prose.md - hermes-config-template.prose.md - hermes-agent-baseline.prose.md 3. Each contract now explicitly instructs to run the helper and interpret the three outcomes 4. States that the bug this replaces was deriving reachability from the remote grep's exit code Files changed (4): - scripts/hermes-reachability-check.sh (standalone mode added) - hermes-key-enforcement.prose.md (reachability section added) - hermes-config-template.prose.md (reachability section added) - hermes-agent-baseline.prose.md (reachability section added)
14 KiB
kind, name, version, description, author
| kind | name | version | description | author |
|---|---|---|---|---|
| enforcement | hermes-key-enforcement | 1.0.0 | 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. | 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_keycustom_providers[].api_key(whennamecontainsharnessorlitellm)auxiliary.*.api_key(whenproviderisharnessor containslitellm)delegation.api_key(whenproviderisharnessor containslitellm)compression.api_key(whenproviderisharnessor containslitellm)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 |
Bearer sk-* key (nginx-fronted) |
✅ VALID — authenticated via nginx :80 (verified 2026-08-09: 401 without key, 200 with) |
http://192.168.68.116/litellm/v1 |
Bearer sk-* key (nginx-fronted) |
✅ CURRENT / CANONICAL — captain-approved migration target; 600s proxy_read_timeout (verified) |
http://192.168.68.116:4000/v1 |
Bearer sk-* key (direct container) |
❌ FORBIDDEN — bypasses nginx; port 4000 direct is not a config path |
All harness/litellm providers MUST use an authenticated nginx-fronted path (/litellm/v1 canonical, /v1 legacy-valid).
Any base_url pointing at :4000 or a bare IP without nginx 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:
# ✅ 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.116orlitellm.sysloggh.net
Standard Pattern
Canonical vault process (2026-07-16): see
litellm-api-keys§ Production Vault Access Process. All agents MUST use theinfisical-gateway.shwrapper (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.
# ✅ 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)
# ❌ 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
Reachability Detection
Before checking for hardcoded keys, verify the host is reachable and can be audited. Use the shared reachability helper from the clone root:
# Run on each host to check reachability (Tanko, Mumuni, Koonimo, Koby)
scripts/hermes-reachability-check.sh <host> "api_key: sk-" "/root/.hermes/"
# Example: scripts/hermes-reachability-check.sh 192.168.68.122 "api_key: sk-" "/root/.hermes/"
# Expected outcomes:
# - UNREACHABLE: SSH connection failed (host is down)
# - VIOLATION: SSH succeeded and found matches (report the finding)
# - COMPLIANT: SSH succeeded and found no matches (no hardcoded keys in config)
#
# NOTE: The bug this replaces was deriving reachability from the remote grep's exit code.
# The correct pattern: remote side always succeeds (grep ...; true), so ssh status = connection only.
Detection Query
Run on any Hermes host to detect violations:
# 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:
# 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_KEYlines from /etc/environment (comment out with# [INFISICAL]) and let theinfisical 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. NOT enforced today: CT 116litellm_config.yamlhas nodefault_key_generate_paramsblock, and a key generated with no explicit models comes back with an empty models list. OPEN policy question: should agent keys expire by default? (captain security-policy decision, raised separately.) - 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).
# NOT currently set in the authority; recommended value. CT 116 litellm_config.yaml has no
# default_key_generate_params block today, and a key generated with no explicit models comes back
# with an EMPTY models list. `models` is a literal key-generation parameter, so this is a value to
# ADD — re-read the live registry at CT 116 /opt/inference-harness/litellm_config.yaml and
# re-verify before applying.
litellm_settings:
default_key_generate_params:
models: ["syslog-auto", "gpu-dense", "gpu-vision", "strix-moe"]
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 | 105 (kagentz) | .14 | mumuni |
Infisical vault | ✅ Fixed | systemd Hermes gateway | 2026-08-29 |
| Koby | 111 | .129 | koby |
Infisical vault | ✅ Fixed (DeepSeek-primary) | infisical run |
23:30 UTC Jul 5 |
| Koonimo | 113 | .114 | koonimo |
Infisical vault | ✅ Fixed | infisical run (migrated 2026-07-11) |
2026-08-09 |
| Abiba | 100 | .65 | abiba-pi |
Infisical vault | ✅ N/A (pi native) | — | 19:44 UTC Jul 5 |
| Kagenz0 | 105 | .14 | — | — | ❌ 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 |
Legacy /v1 |
Status |
|---|---|---|---|
| Mumuni | ✅ harness provider | ✅ auxiliary on /v1 (valid) | ✅ Authenticated (verified 2026-08-09) |
| Tanko | ✅ 5 sections | 0 | ✅ Migrated 2026-08-08, keys 200 |
| Koby | ✅ custom provider (harness name) | — | ✅ External DeepSeek primary (intentional, captain ruling 2026-08-11) |
| Koonimo | ✅ .114 (baggy) | — | ✅ 128K context applied 2026-08-09 |
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):
# 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):
# DO NOT USE post-migration:
EnvironmentFile=/etc/environment
# This pattern was replaced by infisical run -- wrapper
Rotation procedure (one vault operation with this standard):
- Generate new key in LiteLLM:
curl /key/generatewith agent alias - Update Infisical vault:
infisical secrets set LITELLM_API_KEY=sk-NEW --project=agents --env=production - Restart:
systemctl restart hermes-gateway(key auto-injected via wrapper)
Violation Response
- Detect — run detection query above
- Fix — replace
api_key: sk-...withapi_key_env: LITELLM_API_KEYin all harness/litellm sections - Verify —
grep -c "api_key_env" config.yamlshould increase, hardcoded harness keys should be 0 - Restart — gateway must restart to pick up env var
- Confirm — test key against LiteLLM:
curl -H "Authorization: Bearer $KEY" .../v1/models→ 200 - Update — bump the verified table above
- Use safe-mutate — if the fix requires updating vault secrets or restarting the gateway on a remote host, use
safe-mutateto verify current state before mutating.
Related Contracts
hermes-config-template.prose.md— full configuration templatelitellm-health.prose.md— LiteLLM stack health verificationzulip-platform-verification.prose.md— cross-platform agent verificationlitellm-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-botcan push directly to master. All other users must use PRs. - Status check:
PR Pipeline — Authorize → Validate → Review → Mergerequired 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:
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/litellm/v1
model: gpu-vision
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/litellm/v1
model: syslog-auto
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