Compare commits

...
Author SHA1 Message Date
abiba-bot a966f9f16d Merge pull request 'fix: mask credential values in step 2 and step 3 output' (#152) from fix/hermes-key-enforcement-mask-credentials-20261003 into master
PR Pipeline — Authorize → Validate → Review → Merge / auth (push) Successful in 3s
PR Pipeline — Authorize → Validate → Review → Merge / validate (push) Successful in 3s
PR Pipeline — Authorize → Validate → Review → Merge / lint (push) Successful in 10s
PR Pipeline — Authorize → Validate → Review → Merge / ai-review (push) Successful in 5s
PR Pipeline — Authorize → Validate → Review → Merge / gate (push) Successful in 2s
2026-10-03 13:20:57 +00:00
root 79a6e9fcf8 fix: mask credential values in step 2 and step 3 output
PR Pipeline — Authorize → Validate → Review → Merge / auth (pull_request) Successful in 5s
PR Pipeline — Authorize → Validate → Review → Merge / validate (pull_request) Successful in 5s
PR Pipeline — Authorize → Validate → Review → Merge / lint (pull_request) Successful in 11s
PR Pipeline — Authorize → Validate → Review → Merge / ai-review (pull_request) Successful in 3s
PR Pipeline — Authorize → Validate → Review → Merge / gate (pull_request) Successful in 0s
Steps 2 and 3 now mask credential values to a 4-character prefix +
'...MASKED' (e.g. sk-BqRR...MASKED) to avoid printing live keys into
transcripts and logs. The detection is identical - every hit is still
found and reported; only the printed value changes.

Added a masking note to the contract's prose to clarify that masked
values are not truncated output.

Proof (bash -n: OK, block as written for koby):
  /etc/systemd/system/hermes-gateway.service.d/litellm-key.conf.bak-20260716-abiba:2:Environment="LITELLM_API_KEY=sk-BqRR...MASKED"
  /etc/systemd/system/hermes-gateway.service.d/vault.env.conf:4:# [rx Finding 5] .env.vault contains MAPPED names (ZULIP_API_KEY, LITELLM_API_KEY).
  /etc/systemd/system/hermes-gateway.service.d/litellm-key.conf:2:Environment="LITELLM_API_KEY=sk-BqRR...MASKED"
  /etc/systemd/system/hermes.service.d/litellm-key.conf:2:Environment="LITELLM_API_KEY=sk-BqRR...MASKED"
  LITELLM_API_KEY=sk-BqRR...MASKED
  step3: PASS

Hit count unchanged: 4 systemd hits + 1 step3 hit.

Correlation: corr=843dd78328cf306e
2026-10-03 13:18:02 +00:00
abiba-bot 5e15ab0f94 Merge pull request 'fix: hermes-key-enforcement per-agent path resolution' (#151) from fix/hermes-key-enforcement-per-agent-path-20261003 into master
PR Pipeline — Authorize → Validate → Review → Merge / auth (push) Successful in 8s
PR Pipeline — Authorize → Validate → Review → Merge / validate (push) Successful in 6s
PR Pipeline — Authorize → Validate → Review → Merge / lint (push) Successful in 11s
PR Pipeline — Authorize → Validate → Review → Merge / ai-review (push) Successful in 3s
PR Pipeline — Authorize → Validate → Review → Merge / gate (push) Successful in 0s
2026-10-03 13:14:43 +00:00
root fbc8146560 fix: simplify LIVE_GW_PID (read locally, parse locally, liveness check)
PR Pipeline — Authorize → Validate → Review → Merge / auth (pull_request) Successful in 7s
PR Pipeline — Authorize → Validate → Review → Merge / validate (pull_request) Successful in 5s
PR Pipeline — Authorize → Validate → Review → Merge / lint (pull_request) Successful in 14s
PR Pipeline — Authorize → Validate → Review → Merge / ai-review (pull_request) Successful in 3s
PR Pipeline — Authorize → Validate → Review → Merge / gate (pull_request) Successful in 3s
Defect A fix (round 3): The double-quoted ssh command had nested
substitution that made it a parse error. Now reads gateway.pid via
SSH, parses it locally with python3, then tests liveness with a
separate short SSH. This removes the whole class of nested-quoting
bugs.

Defect C fix: Step 2 simplified - each grep runs independently, a
missing per-user dir prints probe-failed, never silent empty.

Proof: bash -n passes; block as written runs for koby (LIVE_GW_PID
resolved, step2=VIOLATION 4 hits, step3=PASS), koonimo (LIVE_GW_PID
resolved, step2=VIOLATION 1 hit, step3=PASS), mumuni (LIVE_GW_PID
resolved, step2=probe-failed per-user dir missing, step3=PASS).

Correlation: corr=ad99803cf327c19a
2026-10-03 12:29:40 +00:00
root 6220797b63 fix: four defects - A (step3 var scope), B (user path), C (guard), D (map)
PR Pipeline — Authorize → Validate → Review → Merge / auth (pull_request) Successful in 6s
PR Pipeline — Authorize → Validate → Review → Merge / validate (pull_request) Successful in 7s
PR Pipeline — Authorize → Validate → Review → Merge / lint (pull_request) Successful in 11s
PR Pipeline — Authorize → Validate → Review → Merge / ai-review (pull_request) Successful in 2s
PR Pipeline — Authorize → Validate → Review → Merge / gate (pull_request) Successful in 3s
Defect A: Step B now uses double-quoted ssh so ${HERMES_HOME} (local) is
interpolated on the runner, not the remote shell where it's unset.
Defect B: SYSTEMD_USER_DIR comes from the static per-agent map, not
/home/${USER} (which is /home/root for root agents).
Defect C: Each scan location gets its own probe-failed line; a missing
per-user dir now prints probe-failed instead of silent empty.
Defect D: USER detection eliminated - the static map is the single source
of truth for both HERMES_HOME and SYSTEMD_USER_DIR.

Correlation: corr=910414eb28b22117
2026-10-03 12:08:29 +00:00
root ab3ad1f03d fix: simplify LIVE_GW_PID resolution (avoid nested quote hell)
PR Pipeline — Authorize → Validate → Review → Merge / auth (pull_request) Successful in 7s
PR Pipeline — Authorize → Validate → Review → Merge / validate (pull_request) Successful in 8s
PR Pipeline — Authorize → Validate → Review → Merge / lint (pull_request) Successful in 9s
PR Pipeline — Authorize → Validate → Review → Merge / ai-review (pull_request) Successful in 8s
PR Pipeline — Authorize → Validate → Review → Merge / gate (pull_request) Successful in 1s
Use simpler python3 -c approach that reads gateway.pid directly, then
checks /proc/$PID existence. Avoids the nested f-string quoting issue.

Correlation: corr=b9f997fb9f6e7914
2026-10-03 11:57:07 +00:00
root 824bb75dc4 fix: split HERMES_HOME (static) from LIVE_GW_PID (step 3 only)
PR Pipeline — Authorize → Validate → Review → Merge / auth (pull_request) Successful in 3s
PR Pipeline — Authorize → Validate → Review → Merge / validate (pull_request) Successful in 5s
PR Pipeline — Authorize → Validate → Review → Merge / ai-review (pull_request) Successful in 4s
PR Pipeline — Authorize → Validate → Review → Merge / lint (pull_request) Successful in 14s
PR Pipeline — Authorize → Validate → Review → Merge / gate (pull_request) Successful in 1s
- HERMES_HOME: always resolvable per agent from explicit map
  (mumuni=/home/hermes/.hermes, koby/koonimo=/root/.hermes, tanko=/home/jerome/.hermes)
  Used by steps 1/1b so they run even when gateway is down
- LIVE_GW_PID: resolved from gateway.pid + liveness check, used ONLY by step 3
- HARD GUARD: empty HERMES_HOME prints probe-failed, never expands to /
- Tanko: config at /home/jerome/.hermes/config.yaml now scanned by construction

Correlation: corr=b9f997fb9f6e7914
2026-10-03 11:55:10 +00:00
root 0812374977 fix: step 2 per-agent systemd paths + resolver no longer guesses
PR Pipeline — Authorize → Validate → Review → Merge / auth (pull_request) Successful in 6s
PR Pipeline — Authorize → Validate → Review → Merge / validate (pull_request) Successful in 10s
PR Pipeline — Authorize → Validate → Review → Merge / lint (pull_request) Successful in 14s
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
Defect 1: Step 2 was scanning hardcoded /root/.config/systemd/user/ which
doesn't exist on tanko (units at /etc/systemd/system/hermes.service.d and
/home/jerome/.config/systemd/user/) or mumuni (units at /etc/systemd/system/
hermes-gateway.service.d). Now scans system-level /etc/systemd/system/*hermes*
AND the per-user systemd dir for the user that runs the units. A missing/empty
scan location renders as probe-failed, never as compliant.

Defect 2: The HERMES_PATH resolver ended with 'else echo /home/jerome/.hermes',
which is tanko's path, handed to EVERY agent that has no live gateway.pid. Now
checks tanko's path too (with liveness check), and if no live gateway.pid can
be resolved, leaves HERMES_PATH empty (probe-failed, not a guessed path).

Correlation: corr=136b6c5778391087
2026-10-03 11:34:40 +00:00
root 7feacfbc3b fix: hermes-key-enforcement per-agent path resolution
PR Pipeline — Authorize → Validate → Review → Merge / auth (pull_request) Successful in 5s
PR Pipeline — Authorize → Validate → Review → Merge / validate (pull_request) Successful in 6s
PR Pipeline — Authorize → Validate → Review → Merge / ai-review (pull_request) Successful in 8s
PR Pipeline — Authorize → Validate → Review → Merge / lint (pull_request) Successful in 14s
PR Pipeline — Authorize → Validate → Review → Merge / gate (pull_request) Successful in 2s
The contract had hardcoded /home/jerome/.hermes and /root/.hermes paths, which
was wrong for agents using different users. This fix:

1. Adds a HERMES_PATH variable that resolves per-agent paths:
   - Checks if /home/hermes/.hermes/gateway.pid exists AND process is alive
   - Checks if /root/.hermes/gateway.pid exists AND process is alive
   - Falls back to /home/jerome/.hermes if neither works

2. Updates steps 1, 1b, and 3 to use ${HERMES_PATH} instead of hardcoded paths

3. Fixes Step 3's nested quoting issue by extracting the PID in a separate
   command, then using it in the cat /proc/$PID/environ command

Per-agent verification (2026-10-03):
- Tanko (.122): HERMES_PATH=/home/jerome/.hermes, Step 1 found 2 violations, Step 3 probe-failed
- Mumuni (.14): HERMES_PATH=/home/hermes/.hermes, all steps pass, Step 3 shows LITELLM_API_KEY
- Koby (.129): HERMES_PATH=/root/.hermes, all steps pass, Step 3 shows LITELLM_API_KEY
- Koonimo (.114): HERMES_PATH=/root/.hermes, all steps pass, Step 3 shows LITELLM_API_KEY

Correlation: corr=bcb049b3a0b6433f
2026-10-03 11:19:30 +00:00
abiba-bot 595e67bda6 Merge pull request 'fix(search): expect the seven engines that actually contribute, not the five that don't' (#149) from fix/search-stack-multiprovider-20261003 into master
PR Pipeline — Authorize → Validate → Review → Merge / auth (push) Successful in 18s
PR Pipeline — Authorize → Validate → Review → Merge / validate (push) Successful in 7s
PR Pipeline — Authorize → Validate → Review → Merge / lint (push) Successful in 18s
PR Pipeline — Authorize → Validate → Review → Merge / ai-review (push) Successful in 5s
PR Pipeline — Authorize → Validate → Review → Merge / gate (push) Successful in 2s
2026-10-03 10:13:04 +00:00
+88 -19
View File
@@ -193,38 +193,107 @@ Run on any Hermes host to detect violations.
**Bounded scan (2026-10-02):** Do NOT recurse the entire `/root/.hermes/` tree. Use `--exclude-dir=state-snapshots` to skip dated snapshot directories. Rationale: a superseded config will always carry a superseded key and will report forever with zero signal content (the koby state-snapshot line has repeated on consecutive days). If you deliberately want to include snapshots, say so in the contract and the report.
```bash
# 1. Check config.yaml for hardcoded harness keys (bounded scan — excludes state-snapshots)
timeout 15 ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no root@<ip> \
"grep -rn 'api_key: sk-' /root/.hermes/ --exclude-dir=state-snapshots --include='config.yaml' | grep -v 'deepseek\|openai\|anthropic\|DEEPSEEK'" \
2>/dev/null
# Per-agent path resolution (2026-10-03): mumuni /home/hermes/.hermes, koonimo/koby /root/.hermes, tanko /home/jerome/.hermes (down)
# HERMES_HOME is ALWAYS resolvable per agent from an explicit map, independent of whether anything is running
# SYSTEMD_USER_DIR is the per-user systemd dir for the agent (from the same map)
# LIVE_GW_PID is needed ONLY by step 3; steps 1/1b use HERMES_HOME and run even when the gateway is down
# HARD GUARD: an empty/unresolved HERMES_HOME must never expand into `/` or an empty glob
# Interpret exit status:
# 0 = match found (violation)
# 1 = no match (pass)
# 124 = timeout (probe-failed, not unreachable)
# 255 = ssh connect failed (unreachable)
# other = probe-failed (record the actual code)
# Step A: Resolve HERMES_HOME and SYSTEMD_USER_DIR from the static per-agent map (no SSH needed)
# mumuni: /home/hermes/.hermes + /home/hermes/.config/systemd/user
# koby/koonimo: /root/.hermes + /root/.config/systemd/user
# tanko: /home/jerome/.hermes + /home/jerome/.config/systemd/user
HERMES_HOME="$(case "<agent>" in
mumuni) echo "/home/hermes/.hermes" ;;
koby|koonimo) echo "/root/.hermes" ;;
tanko) echo "/home/jerome/.hermes" ;;
*) echo "" ;;
esac)"
SYSTEMD_USER_DIR="$(case "<agent>" in
mumuni) echo "/home/hermes/.config/systemd/user" ;;
koby|koonimo) echo "/root/.config/systemd/user" ;;
tanko) echo "/home/jerome/.config/systemd/user" ;;
*) echo "" ;;
esac)"
# Step B: Resolve LIVE_GW_PID (only for step 3) - check if gateway.pid exists and process is alive
# NOTE: Simpler approach - read gateway.pid locally, parse locally, then test liveness with a separate SSH
LIVE_GW_PID="$(ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no root@<ip> \
"if [ -n '${HERMES_HOME}' ] && [ -f '${HERMES_HOME}/gateway.pid' ]; then \
cat '${HERMES_HOME}/gateway.pid'; \
fi" 2>/dev/null | python3 -c 'import json,sys; print(json.load(sys.stdin).get("pid",""))' 2>/dev/null)"
# Test liveness with a second short SSH
if [ -n "$LIVE_GW_PID" ]; then
if ! ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no root@<ip> \
"[ -d /proc/${LIVE_GW_PID} ]" 2>/dev/null; then
LIVE_GW_PID="" # Gateway is down
fi
fi
# 1. Check config.yaml for hardcoded harness keys (bounded scan — excludes state-snapshots)
# Uses HERMES_HOME (not LIVE_GW_PID) so it runs even when the gateway is down
if [ -n "${HERMES_HOME}" ]; then
timeout 15 ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no root@<ip> \
"grep -rn 'api_key: sk-' ${HERMES_HOME}/ --exclude-dir=state-snapshots --include='config.yaml' | grep -v 'deepseek\|openai\|anthropic\|DEEPSEEK'" \
2>/dev/null
# Interpret exit status:
# 0 = match found (violation)
# 1 = no match (pass)
# 124 = timeout (probe-failed, not unreachable)
# 255 = ssh connect failed (unreachable)
# other = probe-failed (record the actual code)
else
echo "probe-failed: could not resolve the Hermes home for <agent>"
fi
# 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)
timeout 15 ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no root@<ip> \
"grep -rn 'litellm/v1/responses' /root/.hermes/config.yaml" 2>/dev/null
# ANY output here = WRONG. Must be 'litellm/v1' without /responses suffix.
# Uses HERMES_HOME (not LIVE_GW_PID) so it runs even when the gateway is down
if [ -n "${HERMES_HOME}" ]; then
timeout 15 ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no root@<ip> \
"grep -rn 'litellm/v1/responses' ${HERMES_HOME}/config.yaml" 2>/dev/null
# ANY output here = WRONG. Must be 'litellm/v1' without /responses suffix.
else
echo "probe-failed: could not resolve the Hermes home for <agent>"
fi
# 2. Check systemd drop-ins for master key leaks (2026-07-05: Tanko had this)
# NOTE: Both greps are inside ONE quoted remote command, separated by ; (not two separate ssh arguments)
# NOTE: Scan system-level /etc/systemd/system/*hermes* AND the per-user systemd dir (from the static map)
# Per-agent systemd user dirs (from the map, measured 2026-10-03):
# koby/koonimo: /root/.config/systemd/user
# tanko: /home/jerome/.config/systemd/user
# mumuni: /home/hermes/.config/systemd/user
# All agents: /etc/systemd/system/ (system-level units)
# A missing/empty scan location must render as probe-failed, never as compliant
# NOTE: Double-quoted ssh command so ${SYSTEMD_USER_DIR} (local var) is interpolated on the runner
# Each grep runs independently; a missing dir prints probe-failed, never silent empty
# NOTE: Credential values are masked to a 4-character prefix + "...MASKED" to avoid printing live keys
# The detection is identical — every hit is still found and reported; only the printed value changes
MASK='sk-[A-Za-z0-9_.-]{4}'
timeout 15 ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no root@<ip> \
"grep -rn 'LITELLM_API_KEY' /root/.config/systemd/user/ 2>/dev/null; grep -rn 'LITELLM_API_KEY=sk-synthetic-litellm-' /root/.config/systemd/ 2>/dev/null" ; true
"grep -rn 'LITELLM_API_KEY' /etc/systemd/system/*hermes* 2>/dev/null | sed -E 's/(${MASK})[A-Za-z0-9_.-]+/\1...MASKED/g'; \
if [ -d '${SYSTEMD_USER_DIR}' ]; then \
grep -rn 'LITELLM_API_KEY' '${SYSTEMD_USER_DIR}/' 2>/dev/null | sed -E 's/(${MASK})[A-Za-z0-9_.-]+/\1...MASKED/g'; \
else \
echo 'probe-failed: per-user systemd dir ${SYSTEMD_USER_DIR} not found'; \
fi" ; true
# 3. Verify running process env matches dedicated key
# NOTE: Single-quoted remote command so $(...) expands on the REMOTE host, not the runner
timeout 15 ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no root@<ip> \
'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' \
&& echo "step3: PASS" || echo "step3: probe-failed (exit $?; see stderr above)"
# NOTE: Uses LIVE_GW_PID (not HERMES_HOME) - only runs when gateway is up
# NOTE: Credential value is masked to a 4-character prefix + "...MASKED" to avoid printing live keys
if [ -n "$LIVE_GW_PID" ]; then
timeout 15 ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no root@<ip> \
"cat /proc/${LIVE_GW_PID}/environ | tr '\0' '\n' | grep LITELLM_API_KEY | sed -E 's/(sk-[A-Za-z0-9_.-]{4})[A-Za-z0-9_.-]+/\1...MASKED/g'" \
&& echo "step3: PASS" || echo "step3: probe-failed (exit $?; see stderr above)"
else
echo "step3: probe-failed (no live gateway.pid for this agent)"
fi
```
If any output from step 2 — **critical violation** (master key leaked). Fix immediately.
> **Masking note (2026-10-03):** Credential values in steps 2 and 3 are deliberately masked to a 4-character prefix + `...MASKED` (e.g. `sk-Cggi...MASKED`) to avoid printing live keys into transcripts and logs. The detection is identical — every hit is still found and reported; only the printed value changes. Do not mistake a masked value for truncated output.
### Negative control (probe-failed vs unreachable) — deterministic
To prove the distinction between a scan timeout and a connection failure, run a command that CANNOT finish in time (sleep 5s) with a 1-second timeout: