Skip to content

feat(hermes): enable webhook platform - #2319

Merged
shunkakinoki merged 2 commits into
mainfrom
feat/enable-hermes-webhooks
Aug 9, 2026
Merged

feat(hermes): enable webhook platform#2319
shunkakinoki merged 2 commits into
mainfrom
feat/enable-hermes-webhooks

Conversation

@shunkakinoki

@shunkakinoki shunkakinoki commented Aug 9, 2026

Copy link
Copy Markdown
Owner

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_SECRET or a local secret during setup.

  • New Features

    • Adds platforms.webhook to config/hermes/config.template.yaml (enabled, port 8644, secret placeholder).
    • Updates config/hermes/hydrate.sh to read HERMES_WEBHOOK_SECRET and inject it into the generated config.
  • Migration

    • Set HERMES_WEBHOOK_SECRET or add the secret at ~/.config/hermes/webhook-secret before hydration.
    • Open/expose port 8644 if running behind a firewall or proxy.

Written for commit 4629c58. Summary will update on new commits.

Review in cubic

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
@indent-zero

indent-zero Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Warning

Indent Zero is shutting down on August 7th. Please migrate over to Indent 2.0 to continue getting PR reviews.

PR Summary

Enables the Hermes webhook platform by appending a platforms.webhook block to the shared config template and wiring the signing secret through the existing activation-time hydration script. This lets the Hermes daemon receive external webhook callbacks on port 8644, using a secret sourced from $WEBHOOK_SECRET or ~/.config/hermes/webhook-secret so the value stays out of the nix store.

  • config/hermes/config.template.yaml: adds a top-level platforms.webhook section with enabled: true, extra.port: 8644, and extra.secret: __WEBHOOK_SECRET__.
  • config/hermes/hydrate.sh: resolves WEBHOOK_SECRET from env/secret file via the existing read_secret helper and adds a sed substitution so the placeholder is replaced when generating ~/.hermes/config.yaml.

Issues

2 potential issues found:

  • Latent: platforms.webhook.enabled: true is unconditional, so client hosts (macOS) will also bind port 8644 even though default.nix distinguishes gateway vs client mode. Trigger: any non-Kyber host running this module. If webhook is only meaningful on the gateway host, gate the block on @mode@ in hydrate.sh. → Autofix
  • Hydration still emits platforms.webhook.enabled: true when WEBHOOK_SECRET and ~/.config/hermes/webhook-secret are both missing; read_secret returns empty and the only bail-out is on GATEWAY_TOKEN, so the webhook listener starts on 8644 with an empty secret:. → Autofix

CI Checks

Waiting for CI checks...


⚡ Autofix All Issues

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@shunkakinoki, you've reached your PR review limit, so we couldn't start this review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1208501a-d018-443f-bfc6-1066592c60dc

📥 Commits

Reviewing files that changed from the base of the PR and between 65f7e40 and 4629c58.

📒 Files selected for processing (1)
  • config/hermes/hydrate.sh
📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added webhook platform configuration on port 8644.
    • Added support for securely injecting the webhook secret during configuration setup.

Walkthrough

Hermes now enables a webhook platform on port 8644. Hydration reads the webhook secret from the environment or secrets directory and substitutes it into the generated configuration.

Changes

Webhook configuration

Layer / File(s) Summary
Webhook settings and secret hydration
config/hermes/config.template.yaml, config/hermes/hydrate.sh
The template adds webhook settings and a __WEBHOOK_SECRET__ placeholder. The hydration script loads WEBHOOK_SECRET from the environment or webhook-secret, then substitutes it into config.yaml.

Estimated code review effort: 2 (Simple) | ~5 minutes

Poem

I’m a rabbit with a webhook tune,
Config hops beneath the moon.
Port 8644 rings bright,
Secrets fill the template right.
Hermes burrows, clean and light.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary change: enabling the Hermes webhook platform.
Description check ✅ Passed The description accurately explains the webhook configuration, secret hydration, port, and migration requirements.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/enable-hermes-webhooks

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@shunkakinoki
shunkakinoki merged commit 123e3f1 into main Aug 9, 2026
1 of 2 checks passed
@shunkakinoki
shunkakinoki deleted the feat/enable-hermes-webhooks branch August 9, 2026 13:26
Comment thread config/hermes/hydrate.sh Outdated
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")}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 884358f and 65f7e40.

📒 Files selected for processing (2)
  • config/hermes/config.template.yaml
  • config/hermes/hydrate.sh

enabled: true
extra:
port: 8644
secret: __WEBHOOK_SECRET__

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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.

Comment thread config/hermes/hydrate.sh Outdated
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")}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 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 || true

Repository: 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
done

Repository: 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"])))
PY

Repository: 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)}))
PY

Repository: 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}))
PY

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

Comment thread config/hermes/hydrate.sh
# Hydrate config.yaml
@sed@ \
-e "s|__CLIPROXY_API_KEY__|${CLIPROXY_API_KEY}|g" \
-e "s|__WEBHOOK_SECRET__|${WEBHOOK_SECRET}|g" \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant