From 98c27aa25c0c8fa60bdcd7053b690f3f4de6970d Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 2 Sep 2026 03:37:47 -0700 Subject: [PATCH] feat(webhooks): stamp the emitting profile on outbound webhook payloads Under a multiplexed gateway every profile's outbound webhooks share one delivery worker, so receivers could not tell which profile fired an event. Add a top-level `profile` field to the payload, resolved at fire time from the bound Hermes home via get_active_profile_name() ("default" outside profiles). Documents the field in the wire-format section. Reported by @vszgdcn8cj-ctrl. Fixes #92674 --- agent/outbound_webhooks.py | 5 +++++ tests/agent/test_outbound_webhooks.py | 17 +++++++++++++++++ website/docs/user-guide/features/hooks.md | 3 ++- 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/agent/outbound_webhooks.py b/agent/outbound_webhooks.py index 0f29d5e50f..cd600832cb 100644 --- a/agent/outbound_webhooks.py +++ b/agent/outbound_webhooks.py @@ -445,8 +445,13 @@ def _serialize_payload( cwd = str(Path.cwd()) except OSError: cwd = "" + # Resolved at fire time from the bound home so a multiplexed gateway's + # receivers can tell which profile emitted the event (#92674). + from hermes_cli.profiles import get_active_profile_name + payload = { "hook_event_name": event, + "profile": get_active_profile_name(), "tool_name": kwargs.get("tool_name"), "tool_input": kwargs.get("args") if isinstance(kwargs.get("args"), dict) else None, "session_id": kwargs.get("session_id") or kwargs.get("parent_session_id") or "", diff --git a/tests/agent/test_outbound_webhooks.py b/tests/agent/test_outbound_webhooks.py index 8b02970e50..39c8a25221 100644 --- a/tests/agent/test_outbound_webhooks.py +++ b/tests/agent/test_outbound_webhooks.py @@ -301,6 +301,23 @@ class TestPayload: assert payload["delivery_id"] == "did_1234" assert payload["timestamp"].endswith("Z") + def test_profile_field_reflects_bound_profile_home(self, tmp_path, monkeypatch): + """Receivers behind a multiplexed gateway need to know which profile + fired (#92674): ``profile`` follows the bound home at fire time.""" + from hermes_constants import reset_hermes_home_override, set_hermes_home_override + + monkeypatch.setenv("HERMES_HOME", str(tmp_path)) + profile_home = tmp_path / "profiles" / "b" + profile_home.mkdir(parents=True) + token = set_hermes_home_override(profile_home) + try: + body = outbound_webhooks._serialize_payload("on_session_end", {}, "did_1") + finally: + reset_hermes_home_override(token) + assert json.loads(body)["profile"] == "b" + body = outbound_webhooks._serialize_payload("on_session_end", {}, "did_2") + assert json.loads(body)["profile"] == "default" + def test_unserialisable_values_stringified(self): body = outbound_webhooks._serialize_payload( "on_session_end", {"weird": object()}, "did_1" diff --git a/website/docs/user-guide/features/hooks.md b/website/docs/user-guide/features/hooks.md index 8ef9e7b180..e30b3b0d39 100644 --- a/website/docs/user-guide/features/hooks.md +++ b/website/docs/user-guide/features/hooks.md @@ -1876,11 +1876,12 @@ Secrets: prefer `secret_env` (the name of an environment variable, typically set ### Wire format -Each firing POSTs a JSON body with the same top-level shape as shell hooks' stdin, plus delivery metadata: +Each firing POSTs a JSON body with the same top-level shape as shell hooks' stdin, plus delivery metadata. `profile` names the Hermes profile that emitted the event (`"default"` outside profiles), so receivers behind a multiplexed gateway can tell profiles apart: ```json { "hook_event_name": "on_session_end", + "profile": "default", "tool_name": null, "tool_input": null, "session_id": "sess_abc123",