Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions gateway/authz_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,7 @@ def _is_user_authorized(

# 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 _auth_env(platform_allow_all_var).lower() in {"true", "1", "yes"}:
if platform_allow_all_var and _platform_gate_env(platform_allow_all_var).lower() in {"true", "1", "yes"}:
return True

# Adapter-verified role auth: the Discord adapter already confirmed the
Expand Down Expand Up @@ -610,13 +610,13 @@ def _is_user_authorized(
return True

# Check platform-specific and global allowlists
platform_allowlist = _auth_env(platform_env_map.get(source.platform, ""))
platform_allowlist = _platform_gate_env(platform_env_map.get(source.platform, ""))
group_user_allowlist = ""
group_chat_allowlist = ""
if source.chat_type in {"group", "forum"}:
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")
group_user_allowlist = _platform_gate_env(platform_group_user_env_map.get(source.platform, ""))
group_chat_allowlist = _platform_gate_env(platform_group_chat_env_map.get(source.platform, ""))
global_allowlist = _platform_gate_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
Expand Down Expand Up @@ -700,7 +700,7 @@ def _is_user_authorized(
if user_id in allowed or "*" in allowed:
return True
# No allowlists configured -- check global allow-all flag
return _auth_env("GATEWAY_ALLOW_ALL_USERS").lower() in {"true", "1", "yes"}
return _platform_gate_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
Expand Down
2 changes: 1 addition & 1 deletion gateway/platforms/qqbot/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -3195,7 +3195,7 @@ def _strip_at_mention(content: str) -> str:
return stripped

def _open_dm_opted_in(self) -> bool:
if os.getenv("GATEWAY_ALLOW_ALL_USERS", "").lower() in {"true", "1", "yes"}:
if _resolve_qq_secret("GATEWAY_ALLOW_ALL_USERS", "").lower() in {"true", "1", "yes"}:
return True
return _resolve_qq_secret("QQ_ALLOW_ALL_USERS", "").lower() in {"true", "1", "yes"}

Expand Down
55 changes: 41 additions & 14 deletions gateway/platforms/signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,19 @@
_signal_send_timeout,
get_scheduler,
)
from agent.secret_scope import UnscopedSecretError, get_secret

logger = logging.getLogger(__name__)


def _startup_env_secret(name: str, default: str = "") -> str:
"""Scope-aware Signal gate read with the default-profile startup fallback."""
try:
val = get_secret(name, default)
return ("" if val is None else str(val)).strip()
except UnscopedSecretError:
return os.getenv(name, default).strip()

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -268,7 +278,7 @@ def __init__(self, config: PlatformConfig):
self.ignore_stories = extra.get("ignore_stories", True)

# Parse allowlists — group policy is derived from presence of group allowlist
group_allowed_str = os.getenv("SIGNAL_GROUP_ALLOWED_USERS", "")
group_allowed_str = _startup_env_secret("SIGNAL_GROUP_ALLOWED_USERS", "")
self.group_allow_from = set(_parse_comma_list(group_allowed_str))

# Mention filter — only respond in groups when the bot account is @mentioned.
Expand All @@ -277,16 +287,32 @@ def __init__(self, config: PlatformConfig):
if _rm_cfg is not None:
self.require_mention = bool(_rm_cfg)
else:
self.require_mention = os.getenv("SIGNAL_REQUIRE_MENTION", "false").lower() in ("true", "1", "yes", "on")
self.require_mention = _startup_env_secret("SIGNAL_REQUIRE_MENTION", "false").lower() in ("true", "1", "yes", "on")

# DM allowlist — mirrors SIGNAL_ALLOWED_USERS checked by run.py.
# Stored here so the reaction hooks can skip unauthorized senders
# (reactions fire before run.py's auth gate, so without this check
# every inbound DM from any contact gets a 👀 reaction).
# "*" means all users allowed (open mode); empty means no restriction
# recorded at adapter level (run.py still enforces auth separately).
dm_allowed_str = os.getenv("SIGNAL_ALLOWED_USERS", "*")
self.dm_allow_from = set(_parse_comma_list(dm_allowed_str))
# (reactions fire before run.py's auth gate). Scoped secret >
# YAML extra > unscoped process env. A scoped miss stays empty;
# "*" is only an explicit open-access opt-in.
try:
from agent.secret_scope import current_secret_scope, is_multiplex_active
if is_multiplex_active() and current_secret_scope() is not None:
scope = current_secret_scope()
if "SIGNAL_ALLOWED_USERS" in scope:
dm_allowed_raw = scope.get("SIGNAL_ALLOWED_USERS") or ""
elif extra.get("allowed_users") is not None:
dm_allowed_raw = extra.get("allowed_users")
else:
dm_allowed_raw = ""
else:
dm_allowed_raw = extra.get("allowed_users")
if dm_allowed_raw is None:
dm_allowed_raw = _startup_env_secret("SIGNAL_ALLOWED_USERS", "")
except Exception:
dm_allowed_raw = extra.get("allowed_users")
if dm_allowed_raw is None:
dm_allowed_raw = _startup_env_secret("SIGNAL_ALLOWED_USERS", "")
self.dm_allow_from = set(_parse_comma_list(str(dm_allowed_raw)))

# HTTP client
self.client: Optional[httpx.AsyncClient] = None
Expand Down Expand Up @@ -1635,16 +1661,17 @@ def _reactions_enabled(self, event: "MessageEvent" = None) -> bool:

Two gates:
1. SIGNAL_REACTIONS env var — set to false/0/no to disable globally.
2. DM allowlist — if SIGNAL_ALLOWED_USERS is set, only react to
messages from senders in that list. This prevents unauthorized
contacts from seeing the 👀 reaction (which fires before run.py's
auth gate and would otherwise reveal that a bot is listening).
2. DM allowlist — react only to senders in SIGNAL_ALLOWED_USERS.
Empty (scoped miss / no allowlist) is closed: no pre-auth 👀.
Explicit "*" remains the open-access opt-in.
"""
if os.getenv("SIGNAL_REACTIONS", "true").lower() in {"false", "0", "no"}:
if _startup_env_secret("SIGNAL_REACTIONS", "true").lower() in {"false", "0", "no"}:
return False
if event is not None:
sender = getattr(getattr(event, "source", None), "user_id", None)
if sender and "*" not in self.dm_allow_from and sender not in self.dm_allow_from:
if "*" in self.dm_allow_from:
return True
if not sender or sender not in self.dm_allow_from:
return False
return True

Expand Down
4 changes: 2 additions & 2 deletions gateway/platforms/weixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -1537,9 +1537,9 @@ async def _process_message(self, message: Dict[str, Any]) -> None:
await self.handle_message(event)

def _open_dm_opted_in(self) -> bool:
if os.getenv("GATEWAY_ALLOW_ALL_USERS", "").lower() in {"true", "1", "yes"}:
if _wx_secret("GATEWAY_ALLOW_ALL_USERS", "").lower() in {"true", "1", "yes"}:
return True
return os.getenv("WEIXIN_ALLOW_ALL_USERS", "").lower() in {"true", "1", "yes"}
return (_wx_secret("WEIXIN_ALLOW_ALL_USERS", "") or "").lower() in {"true", "1", "yes"}

def _is_dm_allowed(self, sender_id: str) -> bool:
if self._dm_policy == "disabled":
Expand Down
2 changes: 1 addition & 1 deletion gateway/platforms/whatsapp_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ def _is_broadcast_chat(chat_id: str) -> bool:

# ------------------------------------------------------------------ gating
def _open_dm_opted_in(self) -> bool:
if os.getenv("GATEWAY_ALLOW_ALL_USERS", "").lower() in {"true", "1", "yes"}:
if (_get_wsecret("GATEWAY_ALLOW_ALL_USERS", default="") or "").lower() in {"true", "1", "yes"}:
return True
return (_get_wsecret("WHATSAPP_ALLOW_ALL_USERS", default="") or "").lower() in {"true", "1", "yes"}

Expand Down
15 changes: 13 additions & 2 deletions gateway/platforms/yuanbao.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,20 @@
next_seq_no,
)
from gateway.session import build_session_key
from agent.secret_scope import UnscopedSecretError, get_secret

logger = logging.getLogger(__name__)


def _yb_secret(name: str, default: str = "") -> str:
"""Scope-aware YUANBAO_* / GATEWAY_* read with unscoped startup fallback."""
try:
val = get_secret(name, default)
except UnscopedSecretError:
val = os.getenv(name)
return ("" if val is None else str(val))


# ---------------------------------------------------------------------------
# Version / platform constants (used in AUTH_BIND and sign-token headers)
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -1282,9 +1293,9 @@ def __init__(
self._group_allow_from = group_allow_from

def _open_dm_opted_in(self) -> bool:
if os.getenv("GATEWAY_ALLOW_ALL_USERS", "").lower() in {"true", "1", "yes"}:
if _yb_secret("GATEWAY_ALLOW_ALL_USERS", "").lower() in {"true", "1", "yes"}:
return True
return os.getenv("YUANBAO_ALLOW_ALL_USERS", "").lower() in {"true", "1", "yes"}
return _yb_secret("YUANBAO_ALLOW_ALL_USERS", "").lower() in {"true", "1", "yes"}

def is_dm_allowed(self, sender_id: str) -> bool:
"""Strict DM authorization — pairing does not imply access."""
Expand Down
4 changes: 2 additions & 2 deletions gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -15423,10 +15423,10 @@ async def _handler(event, _session_key):
return _handler

def _make_default_profile_message_handler(self):
"""Scope a multiplexed default-profile message from ingress onward."""
profile_home = Path(get_hermes_home())
"""Scope primary-transport messages to their routed multiplex profile."""

async def _handler(event):
profile_home = self._resolve_profile_home_for_source(event.source)
with _profile_runtime_scope(profile_home):
return await self._handle_message(event)

Expand Down
6 changes: 3 additions & 3 deletions plugins/platforms/email/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -994,7 +994,7 @@ def _allow_all_senders() -> bool:
truthy = {"true", "1", "yes"}
return (
_get_secret("EMAIL_ALLOW_ALL_USERS", "").strip().lower() in truthy
or os.getenv("GATEWAY_ALLOW_ALL_USERS", "").strip().lower() in truthy
or _get_secret("GATEWAY_ALLOW_ALL_USERS", "").strip().lower() in truthy
)

@staticmethod
Expand All @@ -1009,7 +1009,7 @@ def _allowlist_in_effect() -> bool:
"""
return bool(
_get_secret("EMAIL_ALLOWED_USERS", "").strip()
or os.getenv("GATEWAY_ALLOWED_USERS", "").strip()
or _get_secret("GATEWAY_ALLOWED_USERS", "").strip()
)

async def _dispatch_message(self, msg_data: Dict[str, Any]) -> None:
Expand All @@ -1033,7 +1033,7 @@ async def _dispatch_message(self, msg_data: Dict[str, Any]) -> None:
allowed_raw = _get_secret("EMAIL_ALLOWED_USERS", "").strip()
if not allowed_raw:
if _get_secret("EMAIL_ALLOW_ALL_USERS", "").strip().lower() not in {"true", "1", "yes"} and (
os.getenv("GATEWAY_ALLOW_ALL_USERS", "").strip().lower() not in {"true", "1", "yes"}
_get_secret("GATEWAY_ALLOW_ALL_USERS", "").strip().lower() not in {"true", "1", "yes"}
):
logger.debug(
"[Email] Dropping sender at dispatch — EMAIL_ALLOWED_USERS is unset "
Expand Down
4 changes: 2 additions & 2 deletions plugins/platforms/feishu/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -4371,9 +4371,9 @@ def _admit(self, sender: Any, message: Any) -> Optional[RejectReason]:
return "bot_not_mentioned"

if not is_group:
if os.getenv("FEISHU_ALLOW_ALL_USERS", "").strip().lower() in {"true", "1", "yes"}:
if (_get_scoped_secret("FEISHU_ALLOW_ALL_USERS", "") or "").strip().lower() in {"true", "1", "yes"}:
return None
if os.getenv("GATEWAY_ALLOW_ALL_USERS", "").strip().lower() in {"true", "1", "yes"}:
if (_get_scoped_secret("GATEWAY_ALLOW_ALL_USERS", "") or "").strip().lower() in {"true", "1", "yes"}:
return None
# Empty FEISHU_ALLOWED_USERS is the pairing-mode default from setup:
# forward DMs to gateway intake so the pairing handshake can run.
Expand Down
Loading