mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-30 11:52:04 +00:00
refactor(relay): adopt scope_id wire key (guild_id → scope_id dual-read/write) (#55289)
Gateway half of relay-platform-parity Phase 2.5 (D-Q2.5). The relay wire's platform-neutral scope discriminator is renamed guild_id → scope_id; this is the hermes-agent side of the cross-repo wire-compatible migration. - SessionSource: scope_id is canonical; guild_id kept as @deprecated alias. __post_init__ mirrors the two so all existing SessionSource(guild_id=...) constructors across native adapters keep working unchanged. to_dict dual-WRITES scope_id+guild_id; from_dict dual-READS scope_id ?? guild_id. - relay/adapter.py: capture + outbound metadata dual-read/write scope_id. - relay/ws_transport.py: _frame_to_event dual-reads scope_id ?? guild_id. - docs/relay-connector-contract.md: document scope_id (canonical) + guild_id (deprecated alias) in the §3 SessionSource field table (conformance test). 250 relay+session+contract tests green. Solo lane (relay).
This commit is contained in:
parent
f3d2dfbec6
commit
3a55f66602
4 changed files with 47 additions and 16 deletions
|
|
@ -156,7 +156,8 @@ present (may be `null`); the rest are included only when set.
|
|||
| `chat_topic` | string\|null | yes | Channel topic/description (Discord, Slack). |
|
||||
| `user_id_alt` | string | no | Platform-specific stable alt id (Signal UUID, Feishu union_id). |
|
||||
| `chat_id_alt` | string | no | Alternate chat id (e.g. Signal group internal id). |
|
||||
| `guild_id` | string | no | Discord guild / Slack workspace / Matrix server scope. **REQUIRED for Discord server isolation.** Session-key discriminator. |
|
||||
| `scope_id` | string | no | Platform-neutral **scope** discriminator: Discord guild / Slack workspace / Matrix server. **REQUIRED for Discord/Slack scope isolation.** Session-key discriminator. (Canonical name as of the D-Q2.5 wire migration.) |
|
||||
| `guild_id` | string | no | **Deprecated alias for `scope_id`** — still emitted and read during the cross-repo dual-read/dual-write overlap; readers resolve `scope_id ?? guild_id`. Dropped once both repos deploy on `scope_id`. |
|
||||
| `parent_chat_id` | string | no | Parent channel when `chat_id` refers to a thread. |
|
||||
| `message_id` | string | no | Id of the triggering message (for pin/reply/react). |
|
||||
|
||||
|
|
|
|||
|
|
@ -263,11 +263,11 @@ class RelayAdapter(BasePlatformAdapter):
|
|||
platform_value = getattr(platform, "value", platform)
|
||||
if platform_value and platform_value != "relay":
|
||||
self._platform_by_chat[str(chat)] = str(platform_value)
|
||||
guild = getattr(src, "guild_id", None)
|
||||
guild = getattr(src, "scope_id", None) or getattr(src, "guild_id", None)
|
||||
if guild:
|
||||
self._scope_by_chat[str(chat)] = str(guild)
|
||||
return
|
||||
# DM: no guild_id. Remember the authentic author id for outbound
|
||||
# DM: no scope. Remember the authentic author id for outbound
|
||||
# author-binding resolution (the user we're replying to in this DM).
|
||||
user_id = getattr(src, "user_id", None)
|
||||
if user_id:
|
||||
|
|
@ -279,8 +279,10 @@ class RelayAdapter(BasePlatformAdapter):
|
|||
"""Ensure the outbound metadata carries the discriminator the connector's
|
||||
egress guard needs to resolve the owning tenant. Two cases:
|
||||
|
||||
- GUILD reply: re-attach metadata.guild_id (routing-table resolution).
|
||||
- DM reply: there is no guild_id, so re-attach metadata.user_id — the
|
||||
- GUILD reply: re-attach metadata.scope_id (routing-table resolution;
|
||||
also mirrored to the deprecated metadata.guild_id during the D-Q2.5
|
||||
wire migration so a connector on either side resolves the tenant).
|
||||
- DM reply: there is no scope, so re-attach metadata.user_id — the
|
||||
authentic author id we saw inbound — which the connector resolves to
|
||||
the tenant via the recipient's author binding (resolveByUser). Without
|
||||
one of these, egress is declined as 'target not routed to an onboarded
|
||||
|
|
@ -289,14 +291,16 @@ class RelayAdapter(BasePlatformAdapter):
|
|||
No-op when the relevant value is already present or unknown for this chat.
|
||||
"""
|
||||
meta: Dict[str, Any] = dict(metadata or {})
|
||||
if not meta.get("guild_id"):
|
||||
if not meta.get("scope_id") and not meta.get("guild_id"):
|
||||
scope = self._scope_by_chat.get(str(chat_id))
|
||||
if scope:
|
||||
# D-Q2.5 dual-write: canonical scope_id + deprecated guild_id alias.
|
||||
meta["scope_id"] = scope
|
||||
meta["guild_id"] = scope
|
||||
# DM author-binding discriminator. Only meaningful when there's no guild
|
||||
# (a guild reply resolves by guild_id); harmless to carry otherwise, but
|
||||
# DM author-binding discriminator. Only meaningful when there's no scope
|
||||
# (a guild reply resolves by scope_id); harmless to carry otherwise, but
|
||||
# we only set it when this chat is a known DM and the field is absent.
|
||||
if not meta.get("guild_id") and not meta.get("user_id"):
|
||||
if not meta.get("scope_id") and not meta.get("guild_id") and not meta.get("user_id"):
|
||||
dm_user = self._dm_user_by_chat.get(str(chat_id))
|
||||
if dm_user:
|
||||
meta["user_id"] = dm_user
|
||||
|
|
@ -401,14 +405,14 @@ class RelayAdapter(BasePlatformAdapter):
|
|||
member = payload.get("member") or {}
|
||||
user = (member.get("user") if isinstance(member, dict) else None) or payload.get("user") or {}
|
||||
channel_id = str(payload.get("channel_id") or "")
|
||||
guild_id = payload.get("guild_id")
|
||||
guild_id = payload.get("guild_id") # real Discord interaction field
|
||||
source = SessionSource(
|
||||
platform=Platform.RELAY,
|
||||
chat_id=channel_id,
|
||||
chat_type="channel" if guild_id else "dm",
|
||||
user_id=str(user.get("id")) if isinstance(user, dict) and user.get("id") else None,
|
||||
user_name=str(user.get("username")) if isinstance(user, dict) and user.get("username") else None,
|
||||
guild_id=str(guild_id) if guild_id else None,
|
||||
scope_id=str(guild_id) if guild_id else None, # Discord guild → generic scope slot (D-Q2.5)
|
||||
message_id=str(payload.get("id")) if payload.get("id") else None,
|
||||
)
|
||||
return MessageEvent(text=text, message_type=MessageType.TEXT, source=source)
|
||||
|
|
|
|||
|
|
@ -117,7 +117,8 @@ def _event_from_wire(raw: Dict[str, Any]) -> MessageEvent:
|
|||
chat_topic=src.get("chat_topic"),
|
||||
user_id_alt=src.get("user_id_alt"),
|
||||
chat_id_alt=src.get("chat_id_alt"),
|
||||
guild_id=src.get("guild_id"),
|
||||
# D-Q2.5 dual-read: prefer canonical scope_id, fall back to legacy guild_id.
|
||||
scope_id=src.get("scope_id", src.get("guild_id")),
|
||||
parent_chat_id=src.get("parent_chat_id"),
|
||||
message_id=src.get("message_id"),
|
||||
# Authentic upstream-trust signal: this event arrived over the
|
||||
|
|
|
|||
|
|
@ -110,7 +110,14 @@ class SessionSource:
|
|||
user_id_alt: Optional[str] = None # Platform-specific stable alt ID (Signal UUID, Feishu union_id)
|
||||
chat_id_alt: Optional[str] = None # Signal group internal ID
|
||||
is_bot: bool = False # True when the message author is a bot/webhook (Discord)
|
||||
guild_id: Optional[str] = None # Discord guild / Slack workspace / Matrix server scope
|
||||
# Platform-neutral SCOPE discriminator (Discord guild / Slack workspace /
|
||||
# Matrix server). Drives server/workspace isolation + the relay δ/ε/ζ gate.
|
||||
# Wire migration (D-Q2.5): `scope_id` is the canonical name; `guild_id` is a
|
||||
# deprecated legacy alias kept during the cross-repo dual-read/dual-write
|
||||
# overlap. Both are written by to_dict and read by from_dict (scope_id wins);
|
||||
# the `guild_id` alias is dropped in a follow-up once both repos deploy.
|
||||
scope_id: Optional[str] = None
|
||||
guild_id: Optional[str] = None # @deprecated legacy alias for scope_id (D-Q2.5)
|
||||
parent_chat_id: Optional[str] = None # Parent channel when chat_id refers to a thread
|
||||
message_id: Optional[str] = None # ID of the triggering message (for pin/reply/react)
|
||||
role_authorized: bool = False # True when adapter granted access via role (not user ID)
|
||||
|
|
@ -133,6 +140,16 @@ class SessionSource:
|
|||
# forge it across the wire or have it restored from persistence.
|
||||
delivered_via_upstream_relay: bool = False
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
# D-Q2.5 dual-field reconciliation: `scope_id` is canonical, `guild_id`
|
||||
# is the deprecated alias. Mirror whichever was provided onto the other
|
||||
# (scope_id wins on conflict) so internal readers of EITHER field see the
|
||||
# same value during the cross-repo wire migration overlap.
|
||||
if self.scope_id is None and self.guild_id is not None:
|
||||
self.scope_id = self.guild_id
|
||||
elif self.scope_id is not None:
|
||||
self.guild_id = self.scope_id
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
"""Human-readable description of the source."""
|
||||
|
|
@ -169,8 +186,14 @@ class SessionSource:
|
|||
d["user_id_alt"] = self.user_id_alt
|
||||
if self.chat_id_alt:
|
||||
d["chat_id_alt"] = self.chat_id_alt
|
||||
if self.guild_id:
|
||||
d["guild_id"] = self.guild_id
|
||||
# D-Q2.5 dual-write: emit BOTH the canonical `scope_id` and the
|
||||
# deprecated `guild_id` alias (mirrored in __post_init__) so a connector
|
||||
# on either side of the migration resolves the scope. Drop `guild_id`
|
||||
# in the follow-up once both repos are on `scope_id`.
|
||||
scope = self.scope_id if self.scope_id is not None else self.guild_id
|
||||
if scope:
|
||||
d["scope_id"] = scope
|
||||
d["guild_id"] = scope
|
||||
if self.parent_chat_id:
|
||||
d["parent_chat_id"] = self.parent_chat_id
|
||||
if self.message_id:
|
||||
|
|
@ -192,7 +215,9 @@ class SessionSource:
|
|||
chat_topic=data.get("chat_topic"),
|
||||
user_id_alt=data.get("user_id_alt"),
|
||||
chat_id_alt=data.get("chat_id_alt"),
|
||||
guild_id=data.get("guild_id"),
|
||||
# D-Q2.5 dual-read: prefer the canonical `scope_id`, fall back to the
|
||||
# deprecated `guild_id` alias (a peer not yet migrated still sends it).
|
||||
scope_id=data.get("scope_id", data.get("guild_id")),
|
||||
parent_chat_id=data.get("parent_chat_id"),
|
||||
message_id=data.get("message_id"),
|
||||
profile=data.get("profile"),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue