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
This commit is contained in:
Teknium
2026-09-02 03:37:47 -07:00
parent 600b9c7e7e
commit 98c27aa25c
3 changed files with 24 additions and 1 deletions
+5
View File
@@ -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 "",
+17
View File
@@ -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"
+2 -1
View File
@@ -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",