Files
prose-contracts/hermes-key-enforcement.prose.md
T
root 9a789ab76d
PR Pipeline — Authorize → Validate → Review → Merge / auth (pull_request) Successful in 5s
PR Pipeline — Authorize → Validate → Review → Merge / validate (pull_request) Successful in 7s
PR Pipeline — Authorize → Validate → Review → Merge / lint (pull_request) Successful in 7s
PR Pipeline — Authorize → Validate → Review → Merge / ai-review (pull_request) Successful in 6s
PR Pipeline — Authorize → Validate → Review → Merge / gate (pull_request) Successful in 3s
fix(hermes): separate policy observations from fault findings
Contract design defect: 'uses a non-harness provider' (POLICY) and
'cannot authenticate' (FAULT) were printed as the same violation class.
A policy observation must never be phrased as if the agent were broken.

Changes:
1. Added 'Violation Classification' section to all three contracts
2. Separated POLICY (observation only) from FAULT (requires request-level evidence)
3. Rules:
   - Do NOT infer runtime credential resolution from config text alone
   - Require request-level evidence before calling a FAULT: observed auth failure
     or absence of successful calls
   - If calls are succeeding, output is 'POLICY: uses <provider> directly; calls
     succeeding' - not a violation
   - State what you OBSERVED, not what the field implies

Files changed (3):
- hermes-key-enforcement.prose.md
- hermes-config-template.prose.md
- hermes-agent-baseline.prose.md
2026-09-14 12:32:06 +00:00

15 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_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 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.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.

# ✅ 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.

Violation Classification

When reporting findings, separate POLICY observations from FAULT findings:

POLICY (observation only, not a fault)

  • Agent uses a non-internal-harness provider (e.g., direct DeepSeek, Tencent, OpenRouter)
  • Config text has a field that looks unusual but the agent's calls are succeeding
  • Example: "POLICY: Koonimo uses deepseek directly; calls succeeding in last hour"

FAULT (requires request-level evidence)

  • Agent's calls are failing with auth errors (401/403 in logs)
  • Agent's config has no valid API key AND calls are failing
  • Example: "FAULT: Koby's LiteLLM key expired; 401 observed at 2026-09-14 11:42:00"

Rules

  1. Do NOT infer the runtime's credential resolution from config text alone.
  2. Require request-level evidence before calling something a FAULT: an observed auth failure in the agent's log, or the absence of successful calls in the window.
  3. If calls are succeeding, the correct output is "POLICY: uses directly; calls succeeding" - not a violation.
  4. State what you OBSERVED, not what the field implies.

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_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. NOT enforced today: CT 116 litellm_config.yaml has no default_key_generate_params block, 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):

  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.
  • 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:

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