mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
feat(gateway): plugin platforms declare group-chat allowlist and pairing suppression
Two authz behaviours were hard-coded on Platform.QQBOT/TELEGRAM/... in gateway/authz_mixin.py (_GROUP_CHAT_ENV, _ALLOWLIST_SUPPRESSES_PAIRING), so a plugin-registered platform could not get them and removing the bundled QQ adapter would silently drop QQ_GROUP_ALLOWED_USERS gating and change pairing. PlatformEntry gains group_allowed_chats_env and allowlist_suppresses_pairing; authz_mixin reads them through authz_field (live entry, else the bundled gateway_env.AUTHZ). Every bundled platform that was in the old tables declares them in its AUTHZ, so behaviour is unchanged.
This commit is contained in:
1 parent
e6f57427c1
commit
47d9efe490
19 files changed
+70
-17
No files matched your search
+17
-17
@@ -28,19 +28,11 @@ _TRUTHY = frozenset({"true", "1", "yes"})
|
||||
_BOT_LOOP_GUARD_INIT_LOCK = threading.Lock()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ``<PLATFORM>_ALLOWED_USERS`` / ``<PLATFORM>_ALLOW_ALL_USERS`` come from each platform's
|
||||
# ``PlatformEntry.allowed_users_env`` / ``allow_all_env`` (shared with the pairing allowlist mirror).
|
||||
# Platforms whose env allowlist turns the unauthorized-DM default from "pair" into "ignore". Names
|
||||
# only (the env var is still the plugin's): this is the historical set the allowlist-aware default
|
||||
# covered, so adding a platform here is a behaviour change (Yuanbao was never in it).
|
||||
_ALLOWLIST_SUPPRESSES_PAIRING = frozenset({
|
||||
Platform.TELEGRAM, Platform.DISCORD, Platform.WHATSAPP, Platform.WHATSAPP_CLOUD, Platform.SLACK,
|
||||
Platform.SIGNAL, Platform.EMAIL, Platform.SMS, Platform.MATTERMOST, Platform.MATRIX, Platform.DINGTALK,
|
||||
Platform.FEISHU, Platform.WECOM, Platform.WECOM_CALLBACK, Platform.WEIXIN, Platform.BLUEBUBBLES,
|
||||
Platform.QQBOT,
|
||||
})
|
||||
# ``<PLATFORM>_ALLOWED_USERS`` / ``<PLATFORM>_ALLOW_ALL_USERS``, the group-chat allowlist env and
|
||||
# whether an allowlist suppresses pairing come from each platform's ``PlatformEntry``
|
||||
# (``allowed_users_env`` / ``allow_all_env`` / ``group_allowed_chats_env`` /
|
||||
# ``allowlist_suppresses_pairing``), so a plugin platform declares them like a bundled one.
|
||||
_GROUP_USER_ENV = {Platform.TELEGRAM: "TELEGRAM_GROUP_ALLOWED_USERS"}
|
||||
_GROUP_CHAT_ENV = {Platform.TELEGRAM: "TELEGRAM_GROUP_ALLOWED_CHATS", Platform.QQBOT: "QQ_GROUP_ALLOWED_USERS"}
|
||||
|
||||
|
||||
# Gate reads use the shared per-profile isolated reader (allowlist leak under multiplex, #72348).
|
||||
@@ -59,6 +51,13 @@ def authz_field(name, field, entry):
|
||||
return _field(name, field, entry)
|
||||
|
||||
|
||||
def _group_chat_env(platform, entry=None) -> str:
|
||||
"""The platform's group-chat allowlist env name (``PlatformEntry.group_allowed_chats_env``), or ""."""
|
||||
if platform is None:
|
||||
return ""
|
||||
return authz_field(platform.value, "group_allowed_chats_env", entry or _registry_entry(platform)) or ""
|
||||
|
||||
|
||||
def _registry_entry(platform):
|
||||
"""Platform-registry entry for a (plugin) platform, or None. Discovers first (idempotent): the
|
||||
allowlist env names live only on the entry, so an undiscovered process must not read "no list"."""
|
||||
@@ -553,7 +552,7 @@ class GatewayAuthorizationMixin:
|
||||
# Chat-scoped group allowlists must work with ``user_id is None`` (anonymous admins,
|
||||
# sender_chat posts, channel broadcasts).
|
||||
if is_group and source.chat_id:
|
||||
chat_allowlist_env = _GROUP_CHAT_ENV.get(source.platform, "")
|
||||
chat_allowlist_env = _group_chat_env(source.platform)
|
||||
if chat_allowlist_env and _allows(_coerce_allow_set(_auth_env(chat_allowlist_env)), source.chat_id):
|
||||
return True
|
||||
# config.yaml fallback (``extra.group_allowed_chats``): Telegram observe-unmentioned mode
|
||||
@@ -681,7 +680,7 @@ class GatewayAuthorizationMixin:
|
||||
|
||||
platform_allowlist = _auth_env(platform_allow_env)
|
||||
group_user_allowlist = _auth_env(_GROUP_USER_ENV.get(source.platform, "")) if is_group_or_forum else ""
|
||||
group_chat_allowlist = _auth_env(_GROUP_CHAT_ENV.get(source.platform, "")) if is_group_or_forum else ""
|
||||
group_chat_allowlist = _auth_env(_group_chat_env(source.platform, entry)) if is_group_or_forum else ""
|
||||
global_allowlist = _auth_env("GATEWAY_ALLOWED_USERS")
|
||||
|
||||
if not (platform_allowlist or group_user_allowlist or group_chat_allowlist or global_allowlist):
|
||||
@@ -749,9 +748,10 @@ class GatewayAuthorizationMixin:
|
||||
return "pair"
|
||||
if dm_policy in {"allowlist", "disabled"}:
|
||||
return "ignore"
|
||||
env_key = (authz_field(platform.value, "allowed_users_env", _registry_entry(platform)) or ""
|
||||
if platform in _ALLOWLIST_SUPPRESSES_PAIRING else "")
|
||||
allowlist_keys = [env_key, _GROUP_USER_ENV.get(platform), _GROUP_CHAT_ENV.get(platform), *allowlist_keys]
|
||||
entry = _registry_entry(platform)
|
||||
env_key = (authz_field(platform.value, "allowed_users_env", entry) or ""
|
||||
if authz_field(platform.value, "allowlist_suppresses_pairing", entry) else "")
|
||||
allowlist_keys = [env_key, _GROUP_USER_ENV.get(platform), _group_chat_env(platform, entry), *allowlist_keys]
|
||||
if any(key and _auth_env(key).strip() for key in allowlist_keys):
|
||||
return "ignore"
|
||||
return "pair"
|
||||
@@ -138,6 +138,12 @@ class PlatformEntry:
|
||||
# either resolves "open" without ``allow_all_env`` / GATEWAY_ALLOW_ALL_USERS, startup is refused.
|
||||
# A "" member = that policy is YAML-only (QQ Bot). None = no own policy, never checked.
|
||||
own_policy_env: Optional[tuple] = None
|
||||
# Env var listing group/forum CHAT ids whose traffic is authorized by chat (any sender, user_id
|
||||
# optional): Telegram ``TELEGRAM_GROUP_ALLOWED_CHATS``, QQ ``QQ_GROUP_ALLOWED_USERS``. "" = none.
|
||||
group_allowed_chats_env: str = ""
|
||||
# True: a configured env allowlist turns the unauthorized-DM default from "pair" into "ignore"
|
||||
# (the owner restricted access; unknown senders get no pairing code).
|
||||
allowlist_suppresses_pairing: bool = False
|
||||
# --- send ---
|
||||
# send_message native target syntax, see ``gateway.platforms.send_targets``. A hint, not an
|
||||
# authority like ``parse_target_ref_fn``: None / NOT_EXPLICIT fall back to core's generic id
|
||||
|
||||
@@ -27,4 +27,5 @@ ENV_OVERRIDES = (
|
||||
AUTHZ = {
|
||||
"allowed_users_env": "BLUEBUBBLES_ALLOWED_USERS",
|
||||
"allow_all_env": "BLUEBUBBLES_ALLOW_ALL_USERS",
|
||||
"allowlist_suppresses_pairing": True,
|
||||
}
|
||||
@@ -17,4 +17,5 @@ ENV_OVERRIDES = (
|
||||
AUTHZ = {
|
||||
"allowed_users_env": "DINGTALK_ALLOWED_USERS",
|
||||
"allow_all_env": "DINGTALK_ALLOW_ALL_USERS",
|
||||
"allowlist_suppresses_pairing": True,
|
||||
}
|
||||
@@ -17,4 +17,5 @@ AUTHZ = {
|
||||
"allowed_users_env": "DISCORD_ALLOWED_USERS",
|
||||
"allow_all_env": "DISCORD_ALLOW_ALL_USERS",
|
||||
"allow_bots_env": "DISCORD_ALLOW_BOTS",
|
||||
"allowlist_suppresses_pairing": True,
|
||||
}
|
||||
@@ -17,4 +17,5 @@ ENV_OVERRIDES = (
|
||||
AUTHZ = {
|
||||
"allowed_users_env": "EMAIL_ALLOWED_USERS",
|
||||
"allow_all_env": "EMAIL_ALLOW_ALL_USERS",
|
||||
"allowlist_suppresses_pairing": True,
|
||||
}
|
||||
@@ -22,4 +22,5 @@ AUTHZ = {
|
||||
"allowed_users_env": "FEISHU_ALLOWED_USERS",
|
||||
"allow_all_env": "FEISHU_ALLOW_ALL_USERS",
|
||||
"allow_bots_env": "FEISHU_ALLOW_BOTS",
|
||||
"allowlist_suppresses_pairing": True,
|
||||
}
|
||||
@@ -30,4 +30,5 @@ ENV_OVERRIDES = (
|
||||
AUTHZ = {
|
||||
"allowed_users_env": "MATRIX_ALLOWED_USERS",
|
||||
"allow_all_env": "MATRIX_ALLOW_ALL_USERS",
|
||||
"allowlist_suppresses_pairing": True,
|
||||
}
|
||||
@@ -18,4 +18,5 @@ ENV_OVERRIDES = (
|
||||
AUTHZ = {
|
||||
"allowed_users_env": "MATTERMOST_ALLOWED_USERS",
|
||||
"allow_all_env": "MATTERMOST_ALLOW_ALL_USERS",
|
||||
"allowlist_suppresses_pairing": True,
|
||||
}
|
||||
@@ -43,4 +43,6 @@ AUTHZ = {
|
||||
"allowed_users_env": "QQ_ALLOWED_USERS",
|
||||
"allow_all_env": "QQ_ALLOW_ALL_USERS",
|
||||
"own_policy_env": ("", ""),
|
||||
"group_allowed_chats_env": "QQ_GROUP_ALLOWED_USERS",
|
||||
"allowlist_suppresses_pairing": True,
|
||||
}
|
||||
@@ -21,4 +21,5 @@ ENV_OVERRIDES = (
|
||||
AUTHZ = {
|
||||
"allowed_users_env": "SIGNAL_ALLOWED_USERS",
|
||||
"allow_all_env": "SIGNAL_ALLOW_ALL_USERS",
|
||||
"allowlist_suppresses_pairing": True,
|
||||
}
|
||||
@@ -28,4 +28,5 @@ AUTHZ = {
|
||||
"allowed_users_env": "SLACK_ALLOWED_USERS",
|
||||
"allow_all_env": "SLACK_ALLOW_ALL_USERS",
|
||||
"allow_bots_env": "SLACK_ALLOW_BOTS",
|
||||
"allowlist_suppresses_pairing": True,
|
||||
}
|
||||
@@ -19,4 +19,5 @@ ENV_OVERRIDES = (
|
||||
AUTHZ = {
|
||||
"allowed_users_env": "SMS_ALLOWED_USERS",
|
||||
"allow_all_env": "SMS_ALLOW_ALL_USERS",
|
||||
"allowlist_suppresses_pairing": True,
|
||||
}
|
||||
@@ -24,4 +24,6 @@ AUTHZ = {
|
||||
"allowed_users_env": "TELEGRAM_ALLOWED_USERS",
|
||||
"allow_all_env": "TELEGRAM_ALLOW_ALL_USERS",
|
||||
"allow_bots_env": "TELEGRAM_ALLOW_BOTS",
|
||||
"group_allowed_chats_env": "TELEGRAM_GROUP_ALLOWED_CHATS",
|
||||
"allowlist_suppresses_pairing": True,
|
||||
}
|
||||
@@ -33,8 +33,10 @@ AUTHZ = {
|
||||
"allowed_users_env": "WECOM_ALLOWED_USERS",
|
||||
"allow_all_env": "WECOM_ALLOW_ALL_USERS",
|
||||
"own_policy_env": ("WECOM_DM_POLICY", "WECOM_GROUP_POLICY"),
|
||||
"allowlist_suppresses_pairing": True,
|
||||
}
|
||||
CALLBACK_AUTHZ = {
|
||||
"allowed_users_env": "WECOM_CALLBACK_ALLOWED_USERS",
|
||||
"allow_all_env": "WECOM_CALLBACK_ALLOW_ALL_USERS",
|
||||
"allowlist_suppresses_pairing": True,
|
||||
}
|
||||
@@ -24,4 +24,5 @@ AUTHZ = {
|
||||
"allowed_users_env": "WEIXIN_ALLOWED_USERS",
|
||||
"allow_all_env": "WEIXIN_ALLOW_ALL_USERS",
|
||||
"own_policy_env": ("WEIXIN_DM_POLICY", "WEIXIN_GROUP_POLICY"),
|
||||
"allowlist_suppresses_pairing": True,
|
||||
}
|
||||
@@ -27,4 +27,5 @@ AUTHZ = {
|
||||
"allowed_users_env": "WHATSAPP_ALLOWED_USERS",
|
||||
"allow_all_env": "WHATSAPP_ALLOW_ALL_USERS",
|
||||
"own_policy_env": ("WHATSAPP_DM_POLICY", "WHATSAPP_GROUP_POLICY"),
|
||||
"allowlist_suppresses_pairing": True,
|
||||
}
|
||||
@@ -24,4 +24,5 @@ ENV_OVERRIDES = (
|
||||
AUTHZ = {
|
||||
"allowed_users_env": "WHATSAPP_CLOUD_ALLOWED_USERS",
|
||||
"allow_all_env": "WHATSAPP_CLOUD_ALLOW_ALL_USERS",
|
||||
"allowlist_suppresses_pairing": True,
|
||||
}
|
||||
@@ -51,3 +51,31 @@ def test_declared_own_policy_env_refuses_open_policy_without_allow_all(probe_pla
|
||||
assert _own_policy_open_startup_violation(cfg) == "authzprobe: open policy without allow-all opt-in"
|
||||
monkeypatch.setenv("AUTHZPROBE_ALLOW_ALL_USERS", "true")
|
||||
assert _own_policy_open_startup_violation(cfg) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("declared", [True, False])
|
||||
def test_declared_group_chat_env_and_pairing_suppression(monkeypatch, declared):
|
||||
"""A plugin platform declaring ``group_allowed_chats_env`` / ``allowlist_suppresses_pairing``
|
||||
gets chat-id group authorization and the allowlist-aware "ignore" default; undeclared, neither."""
|
||||
from gateway.platform_registry import PlatformEntry, platform_registry
|
||||
from gateway.run import GatewayRunner
|
||||
|
||||
for var in ("GRPPROBE_ALLOWED_USERS", "GRPPROBE_GROUP_CHATS", "GATEWAY_ALLOW_ALL_USERS", "GATEWAY_ALLOWED_USERS"):
|
||||
monkeypatch.delenv(var, raising=False)
|
||||
fields = dict(group_allowed_chats_env="GRPPROBE_GROUP_CHATS", allowlist_suppresses_pairing=True) if declared else {}
|
||||
platform_registry.register(PlatformEntry(
|
||||
name="grpprobe", label="Group Probe", adapter_factory=lambda cfg: None, check_fn=lambda: True,
|
||||
allowed_users_env="GRPPROBE_ALLOWED_USERS", **fields))
|
||||
try:
|
||||
runner = object.__new__(GatewayRunner)
|
||||
runner.config = GatewayConfig(platforms={Platform("grpprobe"): PlatformConfig(enabled=True)})
|
||||
runner.pairing_store = SimpleNamespace(is_approved=lambda *_a, **_kw: False)
|
||||
group = SessionSource(platform=Platform("grpprobe"), chat_id="g1", chat_type="group", user_id=None,
|
||||
user_name="")
|
||||
monkeypatch.setenv("GRPPROBE_GROUP_CHATS", "g1")
|
||||
assert runner._is_user_authorized(group) is declared
|
||||
monkeypatch.delenv("GRPPROBE_GROUP_CHATS")
|
||||
monkeypatch.setenv("GRPPROBE_ALLOWED_USERS", "owner")
|
||||
assert runner._get_unauthorized_dm_behavior(Platform("grpprobe")) == ("ignore" if declared else "pair")
|
||||
finally:
|
||||
platform_registry.unregister("grpprobe")
|
||||
Reference in new issue
Block a user