mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-20 15:33:54 +00:00
fix(gateway): multiplex secret_scope for authz, Slack, webhooks
Secondary profiles under gateway multiplex keep tokens/allowlists in profile secret_scope, not process os.environ. Auth and Slack were still reading os.getenv, so Slack on a secondary profile failed allowlist and socket mode. Webhook deliver also only looked at default adapters. - Prefer get_secret for allowlists / allow-all flags (authz_mixin) - Slack app token + allowlist via secret_scope with getenv fallback - Wrap secondary profile message handlers in _profile_runtime_scope before auth runs - Resolve home-channel env from secret_scope / PlatformConfig - Webhook deliver falls back to _profile_adapters for target platform - Template key event_type for webhook prompts
This commit is contained in:
parent
c82a196ea9
commit
8fc989b416
4 changed files with 110 additions and 12 deletions
|
|
@ -28,6 +28,21 @@ from gateway.whatsapp_identity import (
|
|||
)
|
||||
|
||||
|
||||
def _auth_env(name: str, default: str = "") -> str:
|
||||
"""Read allowlist/auth env; prefer profile secret_scope under multiplex."""
|
||||
if not name:
|
||||
return default
|
||||
try:
|
||||
from agent.secret_scope import get_secret
|
||||
|
||||
val = get_secret(name)
|
||||
if val is not None and str(val).strip():
|
||||
return str(val).strip()
|
||||
except Exception:
|
||||
pass
|
||||
return (os.getenv(name) or default).strip()
|
||||
|
||||
|
||||
class GatewayAuthorizationMixin:
|
||||
"""User/chat authorization methods for ``GatewayRunner``."""
|
||||
|
||||
|
|
@ -425,7 +440,7 @@ class GatewayAuthorizationMixin:
|
|||
|
||||
# Per-platform allow-all flag (e.g., DISCORD_ALLOW_ALL_USERS=true)
|
||||
platform_allow_all_var = platform_allow_all_map.get(source.platform, "")
|
||||
if platform_allow_all_var and os.getenv(platform_allow_all_var, "").lower() in {"true", "1", "yes"}:
|
||||
if platform_allow_all_var and _auth_env(platform_allow_all_var).lower() in {"true", "1", "yes"}:
|
||||
return True
|
||||
|
||||
# Adapter-verified role auth: the Discord adapter already confirmed the
|
||||
|
|
@ -456,13 +471,13 @@ class GatewayAuthorizationMixin:
|
|||
return True
|
||||
|
||||
# Check platform-specific and global allowlists
|
||||
platform_allowlist = os.getenv(platform_env_map.get(source.platform, ""), "").strip()
|
||||
platform_allowlist = _auth_env(platform_env_map.get(source.platform, ""))
|
||||
group_user_allowlist = ""
|
||||
group_chat_allowlist = ""
|
||||
if source.chat_type in {"group", "forum"}:
|
||||
group_user_allowlist = os.getenv(platform_group_user_env_map.get(source.platform, ""), "").strip()
|
||||
group_chat_allowlist = os.getenv(platform_group_chat_env_map.get(source.platform, ""), "").strip()
|
||||
global_allowlist = os.getenv("GATEWAY_ALLOWED_USERS", "").strip()
|
||||
group_user_allowlist = _auth_env(platform_group_user_env_map.get(source.platform, ""))
|
||||
group_chat_allowlist = _auth_env(platform_group_chat_env_map.get(source.platform, ""))
|
||||
global_allowlist = _auth_env("GATEWAY_ALLOWED_USERS")
|
||||
|
||||
if not platform_allowlist and not group_user_allowlist and not group_chat_allowlist and not global_allowlist:
|
||||
# No env allowlist configured. Adapters that own their own
|
||||
|
|
@ -511,7 +526,7 @@ class GatewayAuthorizationMixin:
|
|||
if effective_policy == "allowlist":
|
||||
return True
|
||||
# No allowlists configured -- check global allow-all flag
|
||||
return os.getenv("GATEWAY_ALLOW_ALL_USERS", "").lower() in {"true", "1", "yes"}
|
||||
return _auth_env("GATEWAY_ALLOW_ALL_USERS").lower() in {"true", "1", "yes"}
|
||||
|
||||
# Telegram can optionally authorize group traffic by chat ID.
|
||||
# Keep this separate from TELEGRAM_GROUP_ALLOWED_USERS, which gates
|
||||
|
|
|
|||
|
|
@ -1131,6 +1131,8 @@ class WebhookAdapter(BasePlatformAdapter):
|
|||
# Special token: dump the entire payload as JSON
|
||||
if key == "__raw__":
|
||||
return json.dumps(payload, indent=2)[:4000]
|
||||
if key == "event_type":
|
||||
return event_type
|
||||
value: Any = payload
|
||||
for part in key.split("."):
|
||||
if isinstance(value, dict):
|
||||
|
|
@ -1279,7 +1281,18 @@ class WebhookAdapter(BasePlatformAdapter):
|
|||
success=False, error=f"Unknown platform: {platform_name}"
|
||||
)
|
||||
|
||||
# Default adapters first; multiplex may park Slack/etc. only on a
|
||||
# secondary profile (self._profile_adapters). Fall back so webhook
|
||||
# deliver:slack still works when default has slack disabled.
|
||||
adapter = self.gateway_runner.adapters.get(target_platform)
|
||||
if not adapter:
|
||||
for _prof, amap in (getattr(self.gateway_runner, "_profile_adapters", None) or {}).items():
|
||||
if not isinstance(amap, dict):
|
||||
continue
|
||||
cand = amap.get(target_platform)
|
||||
if cand is not None:
|
||||
adapter = cand
|
||||
break
|
||||
if not adapter:
|
||||
return SendResult(
|
||||
success=False,
|
||||
|
|
|
|||
|
|
@ -8877,14 +8877,31 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
return connected
|
||||
|
||||
def _make_profile_message_handler(self, profile_name: str):
|
||||
"""Return a message handler that stamps source.profile then delegates."""
|
||||
"""Return a message handler that stamps source.profile then delegates.
|
||||
|
||||
Auth runs inside ``_handle_message`` *before* the agent-turn scope is
|
||||
installed. For secondary profiles under multiplex, wrap the whole
|
||||
handler in ``_profile_runtime_scope`` so allowlists/tokens from that
|
||||
profile's ``.env`` are visible to ``get_secret`` / authz.
|
||||
"""
|
||||
from hermes_cli.profiles import get_profile_dir
|
||||
|
||||
try:
|
||||
profile_home = get_profile_dir(profile_name)
|
||||
except Exception:
|
||||
profile_home = None
|
||||
|
||||
async def _handler(event):
|
||||
try:
|
||||
if getattr(event, "source", None) is not None and not event.source.profile:
|
||||
event.source.profile = profile_name
|
||||
except Exception:
|
||||
pass
|
||||
if profile_home is not None:
|
||||
with _profile_runtime_scope(profile_home):
|
||||
return await self._handle_message(event)
|
||||
return await self._handle_message(event)
|
||||
|
||||
return _handler
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -11813,7 +11830,39 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
if not history and source.platform and source.platform != Platform.LOCAL and source.platform != Platform.WEBHOOK:
|
||||
platform_name = source.platform.value
|
||||
env_key = _home_target_env_var(platform_name)
|
||||
if not os.getenv(env_key):
|
||||
# Multiplex: home channel may live only in the profile secret
|
||||
# scope / PlatformConfig, not process os.environ.
|
||||
home_env = ""
|
||||
try:
|
||||
from agent.secret_scope import get_secret
|
||||
|
||||
home_env = (get_secret(env_key) or "").strip() if env_key else ""
|
||||
except Exception:
|
||||
home_env = ""
|
||||
if not home_env:
|
||||
home_env = (os.getenv(env_key) or "").strip() if env_key else ""
|
||||
# Also honor in-memory / yaml home_channel on this platform.
|
||||
try:
|
||||
if not home_env and self.config.get_home_channel(source.platform):
|
||||
home_env = "set"
|
||||
except Exception:
|
||||
pass
|
||||
# Secondary-profile platforms (e.g. Slack on yolo) may only exist
|
||||
# under that profile's loaded config — check after scope install.
|
||||
if not home_env:
|
||||
try:
|
||||
from hermes_cli.profiles import get_profile_dir
|
||||
from gateway.config import load_gateway_config as _lgc
|
||||
prof = (getattr(source, "profile", None) or "").strip()
|
||||
if prof and prof != "default":
|
||||
# Already inside profile scope for secondary handlers;
|
||||
# re-read live config for home_channel.
|
||||
_pcfg = _lgc()
|
||||
if _pcfg.get_home_channel(source.platform):
|
||||
home_env = "set"
|
||||
except Exception:
|
||||
pass
|
||||
if not home_env:
|
||||
# Slack dispatches all Hermes commands through a single
|
||||
# parent slash command `/hermes`; bare `/sethome` is not
|
||||
# registered and would fail with "app did not respond".
|
||||
|
|
|
|||
|
|
@ -972,7 +972,14 @@ class SlackAdapter(BasePlatformAdapter):
|
|||
return False
|
||||
|
||||
raw_token = self.config.token
|
||||
app_token = os.getenv("SLACK_APP_TOKEN")
|
||||
# Multiplex: profile secrets live in secret_scope, not process os.environ.
|
||||
# Prefer get_secret; fall back to os.getenv for single-profile / tests.
|
||||
try:
|
||||
from agent.secret_scope import get_secret
|
||||
|
||||
app_token = get_secret("SLACK_APP_TOKEN") or os.getenv("SLACK_APP_TOKEN")
|
||||
except Exception:
|
||||
app_token = os.getenv("SLACK_APP_TOKEN")
|
||||
|
||||
if not raw_token:
|
||||
logger.error("[Slack] SLACK_BOT_TOKEN not set")
|
||||
|
|
@ -3986,18 +3993,32 @@ class SlackAdapter(BasePlatformAdapter):
|
|||
if os.getenv("SLACK_ALLOW_ALL_USERS", "").lower() in {"true", "1", "yes"}:
|
||||
return True
|
||||
|
||||
def _env(name: str) -> str:
|
||||
# Multiplex: profile .env is in secret_scope, not process environ.
|
||||
try:
|
||||
from agent.secret_scope import get_secret
|
||||
|
||||
val = get_secret(name)
|
||||
if val is not None and str(val).strip():
|
||||
return str(val).strip()
|
||||
except Exception:
|
||||
pass
|
||||
return (os.getenv(name) or "").strip()
|
||||
|
||||
allowed_ids = set()
|
||||
platform_allowlist = os.getenv("SLACK_ALLOWED_USERS", "").strip()
|
||||
platform_allowlist = _env("SLACK_ALLOWED_USERS")
|
||||
if platform_allowlist:
|
||||
allowed_ids.update(uid.strip() for uid in platform_allowlist.split(",") if uid.strip())
|
||||
global_allowlist = os.getenv("GATEWAY_ALLOWED_USERS", "").strip()
|
||||
global_allowlist = _env("GATEWAY_ALLOWED_USERS")
|
||||
if global_allowlist:
|
||||
allowed_ids.update(uid.strip() for uid in global_allowlist.split(",") if uid.strip())
|
||||
|
||||
if allowed_ids:
|
||||
return "*" in allowed_ids or normalized_user_id in allowed_ids
|
||||
|
||||
return os.getenv("GATEWAY_ALLOW_ALL_USERS", "").lower() in {"true", "1", "yes"}
|
||||
if _env("SLACK_ALLOW_ALL_USERS").lower() in {"true", "1", "yes"}:
|
||||
return True
|
||||
return _env("GATEWAY_ALLOW_ALL_USERS").lower() in {"true", "1", "yes"}
|
||||
|
||||
async def _handle_slash_confirm_action(self, ack, body, action) -> None:
|
||||
"""Handle a slash-confirm button click from Block Kit."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue