Hermes Agent Wiki 非公式・日本語wiki

Webhook

目次

外部サービス(GitHub、GitLab、JIRA、Stripe など)からイベントを受け取り、Hermes のエージェント実行を自動で起こします。webhook アダプターは HTTP サーバーを動かして POST リクエストを受け取り、HMAC 署名を検証し、ペイロードをエージェントへのプロンプトに変換して、応答を送信元または別の設定済みプラットフォームへ返します。

エージェントはイベントを処理したあと、PR にコメントを書き込む、Telegram や Discord にメッセージを送る、結果をログに残す、といった形で応答できます。

動画チュートリアル

YouTube: https://www.youtube.com/embed/WNYe5mD4fY8


早わかり手順

  1. hermes gateway setup または環境変数で有効にします
  2. config.yaml にルートを書くか、hermes webhook subscribe で動的に作ります
  3. 送信元サービスの向き先を http://your-server:8644/webhooks/<route-name> にします

準備

webhook アダプターを有効にする方法は 2 つあります。

セットアップウィザードから

hermes gateway setup

案内に従って webhook を有効にし、ポートと全体の HMAC シークレットを設定します。

環境変数から

~/.hermes/.env に次を追記します。

WEBHOOK_ENABLED=true
WEBHOOK_PORT=8644        # default
WEBHOOK_SECRET=your-global-secret

サーバーの動作確認

ゲートウェイが起動したら、次のコマンドで応答を確かめます。

curl http://localhost:8644/health

期待される応答は次のとおりです。

{"status": "ok", "platform": "webhook"}

ルートの設定

ルートは、webhook の送信元ごとの扱い方を決めるものです。それぞれのルートは config.yaml の platforms.webhook.extra.routes の下に名前付きの項目として書きます。アダプターの設定(port、host、secret、routes)は platforms.webhook: の直下に書くこともできます。どちらの書き方でもアダプターに届き、同じキーが両方にあれば extra: の下に入れた値が優先されます。

ルートの項目

項目 必須 説明
events いいえ 受け付けるイベント種別のリスト(例: ["pull_request"])。空にすると、すべてのイベントを受け付けます。イベント種別は X-GitHub-Event、X-GitLab-Event、またはペイロード内の event_type から読み取ります。
secret はい 署名検証に使う HMAC シークレット。ルートに書かなければ全体の secret が使われます。"INSECURE_NO_AUTH" にすると検証を飛ばしますが、これはテスト専用です。
profile いいえ gateway.multiplex_profiles を有効にしているとき、このルートを実行できるプロファイル。省略すると既定プロファイル専用のルートになります。プロファイル名(たとえば coder)を指定すると、そのルートとシークレットが /p/coder/webhooks/<route> に結び付きます。動的な登録では hermes webhook subscribe <name> --route-profile coder で設定します。
prompt いいえ ペイロードにドット記法でアクセスできるテンプレート文字列(例: {pull_request.title})。省略すると、JSON ペイロード全体がプロンプトに書き出されます。ペイロードの中身は信頼できません。認証済みは信頼済みではありませんを参照してください。
filters いいえ 宣言的なペイロードのふるい分け。認証・本文・イベントによる絞り込みのあと、エージェント実行や直接配信の前に評価されます。条件に合わない場合は HTTP 200 で {"status":"ignored","reason":"filter"} を返します。
script いいえ ~/.hermes/scripts/ に置いた、ふるい分けや変換のためのスクリプト。webhook のペイロードは JSON として標準入力に渡されます。標準出力が JSON オブジェクトならテンプレート展開前にペイロードを差し替え、テキストなら script_output として使えるようになります。標準出力が空、[SILENT]、または終了コードが 0 以外のときは、その webhook を無視します。
skills いいえ エージェント実行時に読み込む skill 名のリスト。
toolsets いいえ ツールセットのキーのリスト(例: ["terminal", "file", "web"])。このルートで起きた実行に限り、プラットフォーム側の webhook 用ツールセットを置き換えます。設定ファイルを手で編集したときだけ効き、hermes webhook subscribe では指定できません。つまりエージェントが作った購読が自分で強い権限を得ることはできません。名前は platform_toolsets の項目と同じ方法で検証されます(知らない名前やプラットフォーム側で制限された名前は捨てられます)。ルートごとのツールセットを参照してください。
deliver いいえ 応答の送り先: github_comment、telegram、discord、slack、signal、sms、whatsapp、matrix、mattermost、homeassistant(プラグイン)、email、dingtalk、feishu、wecom、weixin、bluebubbles、qqbot、または log(既定)。
deliver_extra いいえ 配信の追加設定。キーは deliver の種類によって変わります(例: repo、pr_number、chat_id)。値には prompt と同じ {dot.notation} のテンプレートが使えます。
deliver_only いいえ true にすると、エージェントを一切通しません。展開後の prompt テンプレートが、そのまま配信されるメッセージになります。LLM の費用はゼロで、1 秒未満で届きます。使いどころは直接配信モードを参照してください。deliver に実際の送り先(log 以外)が必要です。
cron_job いいえ イベントのたびに新しい webhook のエージェントセッションを始める代わりに、既存の cron ジョブ(ID か名前で指定)を起動します。展開後の prompt は、その実行限りの一時的な文脈になり、ジョブ自身のプロンプト、スキル、モデル、配信設定が使われます。deliver_only とは同時に使えません。イベントで起動する cron ジョブを参照してください。
coalesce いいえ 同じ対象に続けざまに届く別々のイベントを、待ってまとめ、1 回のエージェント実行にします。ブロックで指定し、key(対象を見分けるペイロードのフィールドかテンプレート。例: pull_request.number)は必須、window_seconds(静かな時間の長さ。既定 30)と max_wait_seconds(実行までの上限。既定 300)は省略できます。イベントのまとめを参照してください。deliver_only や cron_job とは同時に使えません。
mirror_to_session いいえ 既定は false。true にすると、チャットプラットフォームへの配信に成功したあと、届けたメッセージをそのチャットのセッション記録にも書き込みます(続きのやり取りができる cron の報告と同じく、ラベル付きのユーザーの発言として入ります)。そのため、そのチャットで返信すると、エージェントは自分が直前に何を送ったかを分かっています。配信への返信を参照してください。

全体の例

platforms:
  webhook:
    enabled: true
    extra:
      port: 8644
      secret: "global-fallback-secret"
      routes:
        github-pr:
          events: ["pull_request"]
          secret: "github-webhook-secret"
          prompt: |
            Review this pull request:
            Repository: {repository.full_name}
            PR #{number}: {pull_request.title}
            Author: {pull_request.user.login}
            URL: {pull_request.html_url}
            Diff URL: {pull_request.diff_url}
            Action: {action}
          skills: ["github-code-review"]
          deliver: "github_comment"
          deliver_extra:
            repo: "{repository.full_name}"
            pr_number: "{number}"
        deploy-notify:
          events: ["push"]
          secret: "deploy-secret"
          prompt: "New push to {repository.full_name} branch {ref}: {head_commit.message}"
          filters:
            - field: "ref"
              equals: "refs/heads/main"
          deliver: "telegram"

ペイロードのふるい分け

送信元が広い範囲のイベントを送ってくるが、その一部だけでエージェントを動かしたい、あるいは deliver_only の配信を起こしたい、という場合に filters を使います。ふるい分けは、署名の検証・本文の解析・events による絞り込みのあと、プロンプトの展開・重複除去・エージェントへの引き渡し・直接配信より前に走ります。

platforms:
  webhook:
    extra:
      routes:
        todoist:
          events: ["item:updated"]
          secret: "todoist-secret"
          filters:
            - field: "payload.labels"
              contains: "hermes"
            - any:
                - field: "payload.priority"
                  equals: 4
                - field: "payload.project_id"
                  in_file: "~/.hermes/data/todoist/watchlist.json"
          prompt: "Todoist task changed: {payload.content}"

使える演算子は次のとおりです。

  • exists: true|false
  • missing: true
  • equals / not_equals
  • 文字列・リスト・辞書のキーに対する contains
  • その場に書いたリストに対する in
  • JSON 配列、JSON オブジェクト(キーを見ます)、改行区切りのテキストファイルに対する in_file
  • regex
  • all、any、not のグループ

フィールドの指定にはドット記法を使います。payload.foo は、最上位に payload オブジェクトがあればそこから、平坦なペイロードなら webhook 本文の直下から読みます。event / event_type は解決済みのイベント種別と照合し、headers.<Name> はリクエストヘッダーを読みます。

イベントのまとめ

サービス側は、同じ対象について別々のイベントを立て続けに送ってくることがよくあります。1 つのプルリクエストへの 5 回続けての push、1 つのチケットへの編集の連続、出たり消えたりを繰り返す監視アラートなどです。イベントごとに新しい配信 ID が付くので、重複を防ぐキャッシュでは抑えられず、イベントのたびに別々のエージェント実行が起きてしまいます。

ルートに coalesce を設定すると、これらを待ってまとめ、対象ごとに 1 回の実行にできます。

platforms:
  webhook:
    extra:
      routes:
        github-pr:
          events: ["pull_request"]
          secret: "github-webhook-secret"
          coalesce:
            key: "{repository.full_name}#{pull_request.number}"
            window_seconds: 30      # quiet window (default 30)
            max_wait_seconds: 300   # dispatch cap (default 300)
          prompt: "Review PR #{pull_request.number}: {pull_request.title}"
          deliver: "github_comment"
          deliver_extra:
            repo: "{repository.full_name}"
            pr_number: "{pull_request.number}"

仕組み:

  • イベントは、ルートごとに展開後の key でまとめられます。ドットでつないだフィールド名だけ(pull_request.number)でも、テンプレート全体({repository.full_name}#{pull_request.number})でも使えます。
  • 新しいイベントが来るたびに、待っているイベントを置き換え、静かな時間のタイマーを延ばします。window_seconds の間に新しいイベントが来なければ、そのまとまりは最新のイベントのペイロード、プロンプト、配信テンプレートを使って、エージェントを1 回だけ実行します。
  • max_wait_seconds は、まとまりの最初のイベントから数えた待ち時間の上限です。イベントが途切れず届き続けても、実行がいつまでも先送りされることはありません。
  • 2 件以上のイベントがまとめられた場合は、それより前のイベントが何件置き換えられたかを伝える短い注記がプロンプトに付きます。
  • あるイベントで key が解決できない場合(ペイロードにそのフィールドがない場合。例: pull_request.number を key にしたルートに issue_comment イベントが来たとき)、そのイベントはまとめずにすぐ実行されます。関係のない対象が 1 つのまとまりに混ざることはありません。ルートが受け付けるすべてのイベント種別にある key を選んでください。
  • まとめられたリクエストには、HTTP 202 と {"status": "coalesced"} を返します。配信 ID による重複防止は先に働くので、同じ配信をサービス側が再送してきた分は、数に入れずに捨てられます。
  • アダプターの接続が切れたとき(gateway の再接続や hermes gateway stop)、待っているまとまりは捨てずに、すぐ実行されます。ためているイベントはメモリ上にあるので、プロセスを強制終了した場合に失われるのは、せいぜいその時点の待ち時間にためていた分だけです。
  • coalesce はエージェントモードのルートにだけ使えます。deliver_only や cron_job と組み合わせると、起動時に拒否されます。

スクリプトによるふるい分けと変換

宣言的なふるい分けでは足りないときに script を使います。スクリプトは、動作中のプロファイルの ~/.hermes/scripts/ 配下に置く必要があります。相対パスはそこを基準に解決され、そのディレクトリの外へ出るパス指定は遮断されます。.sh と .bash は bash で、それ以外の拡張子は現在の Python インタープリターで実行されます。

ルートのペイロードは JSON として標準入力に送られます。

# ~/.hermes/scripts/todoist-hermes-label.py

payload = json.load(sys.stdin)
labels = payload.get("payload", {}).get("labels", [])
if "hermes" not in labels:
    print("[SILENT]")
    raise SystemExit(0)

payload["body"] = payload["payload"]["content"]
print(json.dumps(payload))

スクリプトの結果は次のように扱われます。

  • 標準出力が JSON オブジェクトなら、prompt と deliver_extra が使うペイロードを差し替えます。
  • 標準出力が JSON ではないテキストなら、script_output としてペイロードに足されます。
  • 標準出力が空、ちょうど [SILENT]、{"__hermes_ignore__": true}、タイムアウト、スクリプトが見つからない、終了コードが 0 以外のいずれかなら、HTTP 200 で {"status":"ignored","reason":"script"} を返します。

プロンプトのテンプレート

プロンプトでは、webhook ペイロードの入れ子になった値にドット記法でアクセスします。

  • {pull_request.title} は payload["pull_request"]["title"] になります
  • {repository.full_name} は payload["repository"]["full_name"] になります
  • {__raw__} — ペイロード全体を字下げ付きの JSON として書き出す特別な記号です(4000 文字で切られます)。監視のアラートや、エージェントに全体の状況が要る汎用の webhook で役に立ちます。
  • 見つからないキーは {key} という文字列のまま残ります(エラーにはなりません)
  • 入れ子の辞書やリストは JSON にしたうえで 2000 文字で切られます

{__raw__} は通常のテンプレート変数と混ぜて使えます。

prompt: "PR #{pull_request.number} by {pull_request.user.login}: {__raw__}"

ルートに prompt テンプレートを設定していない場合は、ペイロード全体が字下げ付きの JSON として書き出されます(4000 文字で切られます)。

同じドット記法のテンプレートは deliver_extra の値でも使えます。

フォーラムのトピックへ配信する

webhook の応答を Telegram へ配信するとき、deliver_extra に message_thread_id(または thread_id)を入れると、特定のフォーラムトピックへ送れます。

webhooks:
  routes:
    alerts:
      events: ["alert"]
      prompt: "Alert: {__raw__}"
      deliver: "telegram"
      deliver_extra:
        chat_id: "-1001234567890"
        message_thread_id: "42"

deliver_extra に chat_id を書かない場合は、送り先プラットフォームに設定したホームチャンネルへ届きます。


GitHub の PR レビュー(手順を追って)

ここでは、プルリクエストごとに自動でコードレビューを走らせる設定を作ります。

1. GitHub 側で webhook を作る

  1. 対象のリポジトリで Settings → Webhooks → Add webhook と進みます
  2. Payload URL に http://your-server:8644/webhooks/github-pr を入れます
  3. Content type を application/json にします
  4. Secret をルートの設定と同じ値にします(例: github-webhook-secret)
  5. Which events? で Let me select individual events を選び、Pull requests にチェックを入れます
  6. Add webhook を押します

2. ルートの設定を足す

上の例のとおり、~/.hermes/config.yaml に github-pr ルートを足します。

3. gh CLI の認証を済ませておく

github_comment の配信は、コメントの投稿に GitHub CLI を使います。

gh auth login

4. 試す

そのリポジトリでプルリクエストを開きます。webhook が飛び、Hermes がイベントを処理して、PR にレビューコメントを書き込みます。


GitLab の webhook 設定

GitLab の webhook もほぼ同じですが、認証のしくみが違います。GitLab はシークレットを X-Gitlab-Token ヘッダーにそのまま入れて送ります(HMAC ではなく、文字列の完全一致で照合します)。

1. GitLab 側で webhook を作る

  1. 対象のプロジェクトで Settings → Webhooks と進みます
  2. URL に http://your-server:8644/webhooks/gitlab-mr を入れます
  3. Secret token を入力します
  4. Merge request events(必要なら他のイベントも)を選びます
  5. Add webhook を押します

2. ルートの設定を足す

platforms:
  webhook:
    enabled: true
    extra:
      routes:
        gitlab-mr:
          events: ["merge_request"]
          secret: "your-gitlab-secret-token"
          prompt: |
            Review this merge request:
            Project: {project.path_with_namespace}
            MR !{object_attributes.iid}: {object_attributes.title}
            Author: {object_attributes.last_commit.author.name}
            URL: {object_attributes.url}
            Action: {object_attributes.action}
          deliver: "log"

配信先の選び方

deliver は、webhook のイベントを処理したあと、エージェントの応答をどこへ送るかを決めます。

配信の種類 説明
log 応答をゲートウェイのログに書き出します。これが既定で、動作を試すときに便利です。
github_comment gh CLI を使って、応答を PR や issue のコメントとして投稿します。deliver_extra.repo と deliver_extra.pr_number が必要です。ゲートウェイを動かしているホストに gh CLI が入っていて、認証済み(gh auth login)である必要があります。
telegram 応答を Telegram へ送ります。ホームチャンネルを使うか、deliver_extra に chat_id を書きます。
discord 応答を Discord へ送ります。ホームチャンネルを使うか、deliver_extra に chat_id を書きます。
slack 応答を Slack へ送ります。ホームチャンネルを使うか、deliver_extra に chat_id を書きます。
signal 応答を Signal へ送ります。ホームチャンネルを使うか、deliver_extra に chat_id を書きます。
sms 応答を Twilio 経由の SMS へ送ります。ホームチャンネルを使うか、deliver_extra に chat_id を書きます。
whatsapp 応答を WhatsApp へ送ります。ホームチャンネルを使うか、deliver_extra に chat_id を書きます。
matrix 応答を Matrix へ送ります。ホームチャンネルを使うか、deliver_extra に chat_id を書きます。
mattermost 応答を Mattermost へ送ります。ホームチャンネルを使うか、deliver_extra に chat_id を書きます。
homeassistant 応答を Home Assistant へ送ります(homeassistant プラグインが必要です)。ホームチャンネル(HASS_HOME_CHANNEL)を使うか、deliver_extra に chat_id を書きます。
email 応答をメールで送ります。ホームチャンネルを使うか、deliver_extra に chat_id を書きます。
dingtalk 応答を DingTalk へ送ります。ホームチャンネルを使うか、deliver_extra に chat_id を書きます。
feishu 応答を Feishu / Lark へ送ります。ホームチャンネルを使うか、deliver_extra に chat_id を書きます。
wecom 応答を WeCom へ送ります。ホームチャンネルを使うか、deliver_extra に chat_id を書きます。
weixin 応答を Weixin(WeChat)へ送ります。ホームチャンネルを使うか、deliver_extra に chat_id を書きます。
bluebubbles 応答を BlueBubbles(iMessage)へ送ります。ホームチャンネルを使うか、deliver_extra に chat_id を書きます。

別のプラットフォームへ送る場合は、送り先のプラットフォームもゲートウェイで有効になっていて、接続済みである必要があります。deliver_extra に chat_id がなければ、そのプラットフォームに設定したホームチャンネルへ応答が届きます。

配信への返信

既定では、配信は送りっぱなしです。Webhook のイベントはそれぞれ別のセッションで動くので、届いたメッセージにそのチャットで返信しても、そこにいるエージェントには何を送ったかの記録がありません。ルートに mirror_to_session: true を設定する(または hermes webhook subscribe に --mirror-to-session を渡す)と、届けた文面が送り先チャットのセッションにも [Webhook delivery: <route>] に続く形で追記されます。そのため「じゃあ彼は休みなの?」のような続きの質問にも文脈が伝わります。

  • この書き写しはできる範囲で行うものです。これが原因で配信が失敗することはなく、そのチャットにまだゲートウェイのセッションがない(そこでエージェントと話した人がいない)場合は行われません。
  • /p/<profile>/ のルートでは、そのプロファイルのチャット用セッションに書き込まれ、別のプロファイルのセッションには書き込まれません。
  • 書き写された文面は、あなたが送ったものとしてチャットの履歴に入ります。deliver_only のルートでは、テンプレートを当てはめただけの生のペイロードがそのまま入ります。会話に入れても構わないと信頼できる内容の送り元(自分のサービスなど。公開の issue トラッカーは避ける)に限って有効にしてください。

直接配信モード

既定では、webhook への POST があるたびにエージェントが動きます。ペイロードがプロンプトになり、エージェントがそれを処理し、その応答が配信されます。つまりイベントごとに LLM のトークンを使います。

考えたり判断したりする必要がなく、そのまま通知を届けたいだけなら、ルートに deliver_only: true を付けます。展開後の prompt テンプレートがそのままメッセージ本文になり、アダプターが設定済みの送り先へ直接届けます。

直接配信が向く場面

  • 外部サービスからの通知 — Supabase / Firebase の webhook がデータベースの変更で飛ぶ → Telegram の利用者へすぐ知らせる
  • 監視のアラート — Datadog / Grafana のアラート webhook → Discord のチャンネルへ流す
  • エージェント同士の合図 — エージェント A が、長い処理が終わったことをエージェント B の利用者へ知らせる
  • バックグラウンド処理の完了 — cron の処理が終わる → 結果を Slack へ投稿する

利点は次のとおりです。

  • LLM のトークンを使わない — エージェントは一度も呼ばれません
  • 1 秒未満で届く — アダプターを 1 回呼ぶだけで、推論のループがありません
  • 安全面はエージェント経由と同じ — HMAC 認証、流量制限、重複除去、本文サイズの上限はすべて同じように効きます
  • 同期で結果が返る — 配信が成功すると POST に 200 OK が返り、送り先が受け付けなければ 502 が返るので、送信側で賢く再送できます

例: Supabase から Telegram へ通知する

platforms:
  webhook:
    enabled: true
    extra:
      port: 8644
      secret: "global-secret"
      routes:
        antenna-matches:
          secret: "antenna-webhook-secret"
          deliver: "telegram"
          deliver_only: true
          prompt: "🎉 New match: {match.user_name} matched with you!"
          deliver_extra:
            chat_id: "{match.telegram_chat_id}"

Supabase の edge function が HMAC-SHA256 でペイロードに署名し、https://your-server:8644/webhooks/antenna-matches へ POST します。webhook アダプターは署名を検証し、ペイロードからテンプレートを展開し、Telegram へ届けて 200 OK を返します。

例: CLI から動的に購読する

hermes webhook subscribe antenna-matches \
  --deliver telegram \
  --deliver-chat-id "123456789" \
  --deliver-only \
  --prompt "🎉 New match: {match.user_name} matched with you!" \
  --description "Antenna match notifications"

応答コード

ステータス 意味
200 OK 配信できました。本文: {"status": "delivered", "route": "...", "target": "...", "delivery_id": "..."}
200 OK(status=duplicate) 重複除去の保持時間(1 時間)のうちに、同じ X-GitHub-Delivery の ID が届きました。再配信はしません。
401 Unauthorized HMAC 署名が不正、または付いていません。
400 Bad Request JSON の本文が壊れています。
404 Not Found 知らないルート名です。
413 Payload Too Large 本文が max_body_bytes を超えました。
429 Too Many Requests ルートの流量制限を超えました。
502 Bad Gateway 送り先のアダプターがメッセージを拒否したか、エラーになりました。詳細はサーバー側のログに残り、応答本文はアダプターの内部が漏れないよう一律で Delivery failed になります。

設定でつまずきやすいところ

  • deliver_only: true には、deliver に実際の送り先が必要です。deliver: log(または deliver の省略)は起動時に弾かれます。設定の誤ったルートが見つかると、アダプターは起動しません。
  • 直接配信モードでは skills は無視されます(エージェントが動かないので、skill を渡す先がありません)。
  • テンプレートの展開は、{__raw__} を含めてエージェント経由のときと同じ {dot.notation} の書き方です。
  • 重複除去は同じ X-GitHub-Delivery / X-Request-ID ヘッダーを見ます。同じ ID での再送は status=duplicate を返し、再配信はしません。

イベントで起動する cron ジョブ

ルートに cron_job を設定すると、イベントが届くたびに既存の cron ジョブを起動できます。決まった間隔で見に行ったり、新しい webhook のエージェントセッションを始めたりする代わりの方法です。これで、予定を組んだどのジョブもイベント駆動の作業に変えられます。予定は取りこぼしを拾うための見回りとして残し(あるいはめったに動かない間隔にして)、実際に何かが変わった瞬間に webhook から起動させる、という使い方です。

しくみは次のとおりです。

  1. イベントは、ほかのルートと同じ HMAC 認証、流量制限、events/filters/script によるふるい分け、重複の除去を通ります。
  2. ルートの prompt テンプレートがペイロードから展開され、その実行限りの一時的な文脈としてジョブに渡されます(cronjob(action='run', prompt=...) と同じ経路です。ジョブに保存されたプロンプトが書き換わることはありません)。
  3. ジョブは、スケジューラーが使うのと同じ「多くても一度だけ」の確保を通して起動します。そのため webhook がまとめて届いても、すでに動いているジョブが二重に起動することはなく、出力はジョブ自身の配信先に届きます。

例: レビューのフィードバックで PR レビューのジョブを起動する

platforms:
  webhook:
    enabled: true
    extra:
      routes:
        pr-feedback:
          events: ["pull_request_review"]
          secret: "github-webhook-secret"
          cron_job: "pr-review-sweeper"        # existing job ID or name
          prompt: |
            PR #{number} in {repository.full_name} received new review feedback
            from {review.user.login}: {review.body}

CLI から

hermes webhook subscribe pr-feedback \
  --events "pull_request_review" \
  --cron-job "pr-review-sweeper" \
  --prompt "PR #{number} received feedback: {review.body}"

ジョブの指定は購読を作るときに検証されるので、打ち間違いはその場で分かります。

注意点

  • cron_job と deliver_only は同時に使えません(ルートに両方を設定すると、アダプターは起動しません)。cron ジョブは自分で配信を受け持ちます。
  • cron_job のルートでは、ルート側の deliver、deliver_extra、skills の項目は無視され、ジョブ自身の設定が使われます。
  • 一時停止中や無効のジョブは起動しません。イベントはログに記録されて捨てられます。
  • POST はすぐに 202 Accepted を返し、ジョブは裏で実行されます。

動的な購読(CLI)

config.yaml に固定で書くルートのほかに、hermes webhook コマンドで購読をその場で作れます。エージェント自身がイベント起点の仕掛けを用意するときに特に便利です。

購読を作る

hermes webhook subscribe github-issues \
  --events "issues" \
  --prompt "New issue #{issue.number}: {issue.title}\nBy: {issue.user.login}\n\n{issue.body}" \
  --deliver telegram \
  --deliver-chat-id "-100123456789" \
  --description "Triage new GitHub issues"

実行すると webhook の URL と、自動生成された HMAC シークレットが返ります。送信元サービスがその URL へ POST するように設定してください。

購読の一覧を見る

hermes webhook list

購読を消す

hermes webhook remove github-issues

購読を試す

hermes webhook test github-issues
hermes webhook test github-issues --payload '{"issue": {"number": 42, "title": "Test"}}'

動的な購読のしくみ

  • 購読は ~/.hermes/webhook_subscriptions.json に保存されます
  • webhook アダプターはリクエストを受けるたびに、このファイルの整合のとれたスナップショットを読み直します(更新時刻・サイズ・inode を見て変化があったときだけ読み、ロックは使いません)
  • 同じ名前があるときは、config.yaml の固定ルートが必ず優先されます
  • 動的な購読は、固定ルートと同じ書式・同じ機能を使えます(イベント、プロンプトのテンプレート、skill、配信先)
  • 作成・更新・有効化と無効化・削除の操作は順番に1つずつ処理され、保存ファイルを丸ごと原子的に置き換えます。同時に走った古い更新は拒否されるので、別の操作が削除や無効化したルートが元に戻ることはありません
  • 既存のルートを --route-profile で付け替えると、その HMAC シークレットは自動で新しくなります。古いプロファイルのシークレットを持つ呼び出し元は、付け替えたルートで認証できません。別の --secret を渡すのは、受け取る側のサービスが運用者の決めた値を求める場合だけにしてください
  • ゲートウェイの再起動は不要です。購読した瞬間から有効になります

エージェントに購読を作らせる

webhook-subscriptions skill の案内があれば、エージェントはターミナルのツールから購読を作れます。「GitHub の issue 用に webhook を設定して」と頼めば、適切な hermes webhook subscribe コマンドを実行します。


ルートごとのツールセット

webhook から始まるエージェント実行は、既定でわざと狭いツールセット(web_search、web_extract、vision_analyze、clarify)になります。webhook のペイロードには第三者が書いた信用できない内容が混ざりうるからです。公開 PR のタイトルや issue のコメントが、プロンプトへの注入を通じてターミナルに手を伸ばせてはいけません。

localhost の監視デーモンがシステムのアラートを送ってくる、社内の CI から送る、といった信用できるルートに限っては、他の webhook ルートを広げずに、そのルートだけ広いツールセットを与えられます。

platforms:
  webhook:
    enabled: true
    extra:
      routes:
        oom-emergency:
          secret: "monitor-secret"
          prompt: "Memory emergency: {detail}. Diagnose with ps/free/py-spy and report."
          toolsets: ["terminal", "file", "code_execution", "web"]
          deliver: "telegram"

動的な購読でこれを使うには、~/.hermes/webhook_subscriptions.json を直接編集して toolsets のキーを足します。

{
  "oom-emergency": {
    "secret": "...",
    "prompt": "...",
    "toolsets": ["terminal", "file", "web"],
    "deliver": "telegram"
  }
}

挙動と安全面の性質は次のとおりです。

  • ルートに書いたリストは、そのルートの実行についてプラットフォーム側の webhook 用ツールセットの解決を置き換えます(併合ではありません)。
  • 名前は platform_toolsets の設定と同じ経路で検証され、知らない名前やプラットフォーム側で制限されたツールセットは捨てられます。
  • hermes webhook subscribe には、ツールセットを指定するフラグをあえて用意していません。強い権限を与えるのは設定ファイルを手で編集したときだけなので、エージェントが実行中に自分で購読を作っても terminal を自分に与えることはできません。
  • 広いツールセットを与えるのは、送信者を完全に自分で握っていて、本物の HMAC シークレットを設定したルートだけにしてください。正しい署名付きのペイロードをそのルートへ POST できる相手は、実質的にそれらのツールを持つエージェントを動かせます。

安全のしくみ

webhook アダプターは、いくつもの層で守りを固めています。

HMAC 署名の検証

アダプターは、送信元ごとに適した方法で受信した webhook の署名を検証します。

  • GitHub: X-Hub-Signature-256 ヘッダー — sha256= を先頭に付けた HMAC-SHA256 の 16 進ダイジェスト
  • GitLab: X-Gitlab-Token ヘッダー — シークレット文字列そのものの照合
  • Standard Webhooks: webhook-id、webhook-timestamp、webhook-signature の各ヘッダー — 署名の対象は {id}.{timestamp}.{raw_body} で、署名は v1,<base64-hmac-sha256> の形です
  • 汎用(V2・推奨): X-Webhook-Signature-V2 と X-Webhook-Timestamp のヘッダー — <timestamp>.<body> の HMAC-SHA256 の 16 進ダイジェスト。タイムスタンプ(Unix 秒)はサーバー時計の ±300 秒以内である必要があり、これによって盗まれたリクエストの再送を防ぎます。
  • 汎用(V1・旧式): X-Webhook-Signature ヘッダー — 本文だけの HMAC-SHA256 の 16 進ダイジェスト。後方互換のために今も受け付けますが、再送への守りがありません(盗まれたリクエストがいつまでも通ります)。ゲートウェイはルートごとに 1 回、非推奨の警告をログに出します。送信側を V2 へ切り替えてください。

シークレットを設定しているのに、認識できる署名ヘッダーが 1 つもない場合、そのリクエストは拒否されます。

シークレットは必須

どのルートにもシークレットが要ります。ルートに直接書くか、全体の secret を受け継ぐかのどちらかです。シークレットのないルートがあると、アダプターは起動時にエラーで止まります。開発やテストのときに限り、シークレットを "INSECURE_NO_AUTH" にして検証を完全に飛ばせます。

複数プロファイルの振り分けを有効にしている場合、ルートの profile 項目は そのシークレットを 1 つの実行先に結び付けます。profile のないルートは 既定プロファイル専用です。ルートの署名が正しくても、/p/<profile>/ の 接頭辞がルートの結び付きと合わなければ、そのリクエストは拒否されます。

INSECURE_NO_AUTH は、ゲートウェイがループバック(127.0.0.1、localhost、::1)で待ち受けているときだけ受け付けられます。0.0.0.0 や LAN の IP のようにループバック以外で待ち受けている状態で組み合わせると、アダプターは起動しません。認証なしの入口をうっかり外向けに開いてしまうのを防ぐためです。

流量制限

ルートごとに、既定で 1 分あたり 30 リクエストに制限されます(固定の時間枠で数えます)。全体の設定を変えるには次のようにします。

platforms:
  webhook:
    extra:
      rate_limit: 60  # requests per minute

上限を超えたリクエストには 429 Too Many Requests が返ります。

重複の除去

配信 ID(X-GitHub-Delivery、svix-id、webhook-id、X-Request-ID、またはリクエストごとのランダムな ID で代用)は 1 時間保持されます。重複した配信(webhook の再送など)は黙って読み飛ばして 200 を返すので、エージェントが二重に動くことはありません。

本文サイズの上限

1 MB を超えるペイロードは、本文を読む前に拒否されます。変えるには次のようにします。

platforms:
  webhook:
    extra:
      max_body_bytes: 2097152  # 2 MB

認証済みは信頼済みではありません


困ったとき

webhook が届かない

  • ポートが公開されていて、webhook の送信元から届く状態か確かめます
  • ファイアウォールの設定を見ます。8644(または設定したポート)が開いている必要があります
  • URL のパスが http://your-server:8644/webhooks/<route-name> と一致しているか確かめます
  • /health の入口を叩いて、サーバーが動いていることを確かめます

署名の検証に失敗する

  • ルートの設定にあるシークレットが、webhook の送信元に設定したシークレットと完全に一致しているか確かめます
  • GitHub の場合は HMAC を使うので、X-Hub-Signature-256 を見ます
  • GitLab の場合はトークン文字列の照合なので、X-Gitlab-Token を見ます
  • ゲートウェイのログに Invalid signature の警告が出ていないか確かめます

イベントが無視される

  • そのイベント種別が、ルートの events に入っているか確かめます
  • GitHub のイベントは pull_request、push、issues のような値です(X-GitHub-Event ヘッダーの値)
  • GitLab のイベントは merge_request、push のような値です(X-GitLab-Event ヘッダーの値)
  • events が空、または設定されていなければ、すべてのイベントを受け付けます

エージェントが応答しない

  • ゲートウェイを前面で動かしてログを見ます: hermes gateway run
  • プロンプトのテンプレートが正しく展開されているか確かめます
  • 送り先が設定済みで、接続できているか確かめます

応答が二重に届く

  • 重複除去のしくみで防げるはずです。webhook の送信元が配信 ID のヘッダー(X-GitHub-Delivery、svix-id、webhook-id、X-Request-ID)を送っているか確かめます
  • 配信 ID は 1 時間保持されます

gh CLI のエラー(GitHub コメントの配信)

  • ゲートウェイを動かしているホストで gh auth login を実行します
  • 認証した GitHub の利用者が、そのリポジトリへの書き込み権限を持っているか確かめます
  • gh が入っていて、PATH に通っているか確かめます

環境変数

変数 説明 既定値
WEBHOOK_ENABLED webhook のプラットフォームアダプターを有効にします false
WEBHOOK_PORT webhook を受け取る HTTP サーバーのポート 8644
WEBHOOK_SECRET 全体で使う HMAC シークレット(ルートに指定がないときの受け皿) _(なし)_