feat(hermes): enable webhook platform - #2319
Conversation
Add webhook platform configuration to the Hermes config template with configurable port (8644) and secret (hydrated from ~/.config/hermes/webhook-secret). Changes: - config.template.yaml: add platforms.webhook section - hydrate.sh: add WEBHOOK_SECRET substitution
|
Warning Indent Zero is shutting down on August 7th. Please migrate over to Indent 2.0 to continue getting PR reviews.
|
|
Warning Review limit reached
Next review available in: 56 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughSummary by CodeRabbit
WalkthroughHermes now enables a webhook platform on port ChangesWebhook configuration
Estimated code review effort: 2 (Simple) | ~5 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| TELEGRAM_TOKEN="${HERMES_TELEGRAM_TOKEN:-${TELEGRAM_TOKEN:-$(read_secret "${SECRETS_DIR}/telegram-token")}}" | ||
| GATEWAY_TOKEN="${HERMES_GATEWAY_TOKEN:-${GATEWAY_TOKEN:-$(read_secret "${SECRETS_DIR}/gateway-token")}}" | ||
| WHATSAPP_ALLOW_FROM="${WHATSAPP_ALLOW_FROM:-$(read_secret "${SECRETS_DIR}/whatsapp-allow-from")}" | ||
| WEBHOOK_SECRET="${WEBHOOK_SECRET:-$(read_secret "${SECRETS_DIR}/webhook-secret")}" |
There was a problem hiding this comment.
Webhook can hydrate without a secret: read_secret returns "" when neither $WEBHOOK_SECRET nor ~/.config/hermes/webhook-secret is present, and the only guard below (line 68) exits on missing GATEWAY_TOKEN. The resulting config.yaml will contain secret: (null) while platforms.webhook.enabled: true is baked into the template, so the daemon opens 8644 with no signing secret. Consider either skipping/disabling the webhook block when the secret is empty, or aborting hydration the way you do for GATEWAY_TOKEN.
| api_mode: chat_completions | ||
| platforms: | ||
| webhook: | ||
| enabled: true |
There was a problem hiding this comment.
Webhook enabled on all hosts: default.nix sets mode to gateway on Kyber and client on macOS, but hydrate.sh never branches on @mode@ for this block, so client hosts will also bind TCP 8644. If the webhook endpoint is only reachable via the gateway host's public URL, consider gating this section on mode (or on WEBHOOK_SECRET being non-empty) inside hydrate.sh.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@config/hermes/config.template.yaml`:
- Line 335: Update the webhook secret substitution near the secret configuration
so the generated YAML always treats the value as a string: quote the substituted
__WEBHOOK_SECRET__ value and apply YAML escaping, or enforce and document a safe
token format before substitution. Preserve the existing secret value while
preventing YAML booleans, nulls, indicators, and special characters from being
misparsed.
In `@config/hermes/hydrate.sh`:
- Line 76: Escape WEBHOOK_SECRET for sed replacement semantics before
interpolating it into the substitution expression, handling &, |, and backslash
characters without altering the intended secret value. Use the escaped value in
the existing __WEBHOOK_SECRET__ replacement within hydrate.sh.
- Line 66: Update the WEBHOOK_SECRET resolution and hydration logic in
hydrate.sh to detect an empty resolved secret before writing
platforms.webhook.secret; when neither configured source provides a value,
reject hydration or disable the webhook instead of leaving it enabled with an
empty secret.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 17cdc9cb-fba4-40be-bb47-61849bf74e0b
📒 Files selected for processing (2)
config/hermes/config.template.yamlconfig/hermes/hydrate.sh
| enabled: true | ||
| extra: | ||
| port: 8644 | ||
| secret: __WEBHOOK_SECRET__ |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Quote and escape the webhook secret.
secret: __WEBHOOK_SECRET__ is an unquoted YAML scalar. Values such as true, null, #value, or values containing YAML indicators can parse incorrectly or invalidate the generated configuration. Quote and YAML-escape the substituted value, or enforce a documented safe token format before substitution.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@config/hermes/config.template.yaml` at line 335, Update the webhook secret
substitution near the secret configuration so the generated YAML always treats
the value as a string: quote the substituted __WEBHOOK_SECRET__ value and apply
YAML escaping, or enforce and document a safe token format before substitution.
Preserve the existing secret value while preventing YAML booleans, nulls,
indicators, and special characters from being misparsed.
| TELEGRAM_TOKEN="${HERMES_TELEGRAM_TOKEN:-${TELEGRAM_TOKEN:-$(read_secret "${SECRETS_DIR}/telegram-token")}}" | ||
| GATEWAY_TOKEN="${HERMES_GATEWAY_TOKEN:-${GATEWAY_TOKEN:-$(read_secret "${SECRETS_DIR}/gateway-token")}}" | ||
| WHATSAPP_ALLOW_FROM="${WHATSAPP_ALLOW_FROM:-$(read_secret "${SECRETS_DIR}/whatsapp-allow-from")}" | ||
| WEBHOOK_SECRET="${WEBHOOK_SECRET:-$(read_secret "${SECRETS_DIR}/webhook-secret")}" |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate files =="
fd -a 'hydrate\.sh$|default\.nix$' . | sed 's#^\./##'
echo "== hydrate.sh outline/size =="
if [ -f config/hermes/hydrate.sh ]; then
wc -l config/hermes/hydrate.sh
sed -n '1,140p' config/hermes/hydrate.sh | nl -ba
fi
echo "== default.nix relevant =="
if [ -f config/hermes/default.nix ]; then
wc -l config/hermes/default.nix
sed -n '1,220p' config/hermes/default.nix | nl -ba
fi
echo "== search websocket/webhook configs =="
rg -n "webhook|SECRET|config\.yaml|hermes|read_secret" config/hermes nix 2>/dev/null || trueRepository: shunkakinoki/dotfiles
Length of output: 9622
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== config/hermes/hydrate.sh =="
awk '{printf "%5d %s\n", NR, $0}' config/hermes/hydrate.sh
echo "== config/hermes/default.nix =="
awk 'NR<=240 {printf "%5d %s\n", NR, $0}' config/hermes/default.nix
echo "== hermes-wide references =="
for file in $(git ls-files | rg '(^|/)config/hermes/|home-manager/services/hermes/'); do
case "$file" in
*.nix|*.sh)
echo "--- $file"
rg -n "hermes|hydra|hydrate|webhook|config.yaml|read_secret|activate|run|start|systemctl|journalctl|curl|jq|sed" "$file" || true
;;
esac
doneRepository: shunkakinoki/dotfiles
Length of output: 8623
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate hermes config/template files =="
git ls-files | rg 'hermes.*(template|config)|config\/hermes|home-manager\/services\/hermes'
echo "== config.template.yaml if present =="
if [ -f config/hermes/config.template.yaml ]; then
awk 'NR<=220 {printf "%5d %s\n", NR, $0}' config/hermes/config.template.yaml
else
fd -i 'config\.template\.yaml$' .
fi
echo "== env template if present =="
if [ -f config/hermes/env.template ]; then
awk 'NR<=220 {printf "%5d %s\n", NR, $0}' config/hermes/env.template
fi
echo "== hermes-dashboard-proxy.conf if present =="
if [ -f home-manager/services/hermes/hermes-dashboard-proxy.conf ]; then
awk '{printf "%5d %s\n", NR, $0}' home-manager/services/hermes/hermes-dashboard-proxy.conf
fi
echo "== behavioral probe for current hydration secret assignment =="
python3 - <<'PY'
def read_secret(files):
for f in files:
try:
return open(f).read().replace("\n", "").replace("\r", "")
except FileNotFoundError:
continue
return ""
def current_webhook_secret(mode='client', hermes_webhook_secret=None, secrets_dir_contains_secret=False):
secret_file_value = "hermes-webhook-secret" if secrets_dir_contains_secret else None
return hermes_webhook_secret if hermes_webhook_secret else read_secret([f"{secrets_dir_contains_secret and '/tmp/.config/hermes/webhook-secret' or '/tmp/.config/hermes/webhook-secret'}"]) if not hermes_webhook_secret else None
envs = [
("missing env only missing file", None, False),
("missing env with file", None, True),
("env present empty", "", False),
]
import os
for name, env_value, has_file in envs:
os.environ.pop("HERMES_WEBHOOK_SECRET", None)
if env_value is not None:
os.environ["HERMES_WEBHOOK_SECRET"] = env_value
with open("/tmp/.config/hermes/webhook-secret", "w") as fh:
fh.write("hermes-webhook-secret")
if not has_file:
os.remove("/tmp/.config/hermes/webhook-secret")
print(name, repr(env_value), "has_file=", has_file, "result=", repr(os.environ.get("HERMES_WEBHOOK_SECRET", "<unset>") or read_secret(["/tmp/.config/hermes/webhook-secret"])))
PYRepository: shunkakinoki/dotfiles
Length of output: 7956
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== tracked paths under config/hermes =="
git ls-files config/hermes | sort
echo "== occurrences of webhook/webhook secret in config/hermes =="
git ls-files config/hermes | xargs rg -n "webhook|WEBHOOK|secret" || true
echo "== occurrences of hermes agent / hermes gateway in service activation =="
rg -n "hermes gateway|hermes-agent|homeDir/.hermes/sessions|homeDir/.hermes" home-manager/services/hermes home-manager/modules/services if [ -d home-manager/modules/services ]; then true; fi || true
echo "== read-only simulation of current shell resolution =="
python3 - <<'PY'
import json, os, sys, tempfile
os.environ.pop("HERMES_WEBHOOK_SECRET", None)
def read_secret(files):
for file in files:
try:
with open(file, "r") as f:
return f.read().replace("\n", "").replace("\r", "")
except FileNotFoundError:
continue
return ""
def webhook_secret(env=None, secret_present=True):
if env is None:
os.environ.pop("HERMES_WEBHOOK_SECRET", None)
elif env != "":
os.environ["HERMES_WEBHOOK_SECRET"] = env
else:
os.environ["HERMES_WEBHOOK_SECRET"] = ""
with tempfile.TemporaryDirectory() as tmpdir:
secret_file = os.path.join(tmpdir, ".config", "hermes", "webhook-secret")
os.makedirs(os.path.dirname(secret_file), exist_ok=True)
if secret_present:
with open(secret_file, "w") as f:
f.write("hermes-webhook-secret")
result = os.environ.get("HERMES_WEBHOOK_SECRET", "") or read_secret([secret_file])
return result
for description, env, secret_present in [
("no env variable, no secret file", None, False),
("no env variable, secret file exists", None, True),
("explicit empty env variable", "", False),
]:
print(json.dumps({"description": description, "value": webhook_secret(env=env, secret_present=secret_present)}))
PYRepository: shunkakinoki/dotfiles
Length of output: 1709
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== config/hermes/config.template.yaml webhook section =="
awk 'NR>=320 && NR<=340 {printf "%5d %s\n", NR, $0}' config/hermes/config.template.yaml
echo "== config/hermes/config.tpl.yaml webhook section =="
awk 'NR>=320 && NR<=340 {printf "%5d %s\n", NR, $0}' config/hermes/config.pl.yaml || awk 'NR>=320 && NR<=340 {printf "%5d %s\n", NR, $0}' config/hermes/config.tpl.yaml
echo "== home-manager/services/hermes service section =="
awk 'NR>=1 && NR<=95 {printf "%5d %s\n", NR, $0}' home-manager/services/hermes/default.nix
echo "== read-only simulation of current shell resolution =="
python3 - <<'PY'
import json, os, sys, tempfile, shutil
def read_secret(files):
for file in files:
try:
with open(file, "r") as f:
return f.read().replace("\n", "").replace("\r", "")
except FileNotFoundError:
continue
return ""
for description, env, secret_present in [
("no env variable, no secret file", None, False),
("no env variable, secret file exists", None, True),
("explicit empty env variable", "", False),
]:
with tempfile.TemporaryDirectory() as tmpdir:
secret_file = os.path.join(tmpdir, ".config", "hermes", "webhook-secret")
os.makedirs(os.path.dirname(secret_file), exist_ok=True)
if secret_present:
with open(secret_file, "w") as f:
f.write("hermes-webhook-secret")
if env is None:
env_value = read_secret([secret_file])
else:
env_value = env or read_secret([secret_file])
print(json.dumps({"description": description, "secret_present": secret_present, "webhook_secret": env_value}))
PYRepository: shunkakinoki/dotfiles
Length of output: 5225
Fail closed when WEBHOOK_SECRET is missing.
When both HERMES_WEBHOOK_SECRET and ${HOME}/.config/hermes/webhook-secret are missing, hydration writes an empty secret into platforms.webhook.secret while leaving platforms.webhook.enabled: true. Check for an empty resolved value before writing the config, and either reject hydration or disable the webhook when no secret is available.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@config/hermes/hydrate.sh` at line 66, Update the WEBHOOK_SECRET resolution
and hydration logic in hydrate.sh to detect an empty resolved secret before
writing platforms.webhook.secret; when neither configured source provides a
value, reject hydration or disable the webhook instead of leaving it enabled
with an empty secret.
| # Hydrate config.yaml | ||
| @sed@ \ | ||
| -e "s|__CLIPROXY_API_KEY__|${CLIPROXY_API_KEY}|g" \ | ||
| -e "s|__WEBHOOK_SECRET__|${WEBHOOK_SECRET}|g" \ |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Escape WEBHOOK_SECRET before using it as a sed replacement.
The raw value is interpolated into the replacement expression. A secret containing &, |, or \ can change the generated value or make hydration fail. Escape the replacement value before constructing the sed expression. This is a substitution bug, not shell command injection.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@config/hermes/hydrate.sh` at line 76, Escape WEBHOOK_SECRET for sed
replacement semantics before interpolating it into the substitution expression,
handling &, |, and backslash characters without altering the intended secret
value. Use the escaped value in the existing __WEBHOOK_SECRET__ replacement
within hydrate.sh.
Enable webhook platform
Summary by cubic
Enables the Hermes webhook platform with a configurable port and secret to support incoming webhook integrations. Config is hydrated from
HERMES_WEBHOOK_SECRETor a local secret during setup.New Features
platforms.webhooktoconfig/hermes/config.template.yaml(enabled, port8644, secret placeholder).config/hermes/hydrate.shto readHERMES_WEBHOOK_SECRETand inject it into the generated config.Migration
HERMES_WEBHOOK_SECRETor add the secret at~/.config/hermes/webhook-secretbefore hydration.8644if running behind a firewall or proxy.Written for commit 4629c58. Summary will update on new commits.