From dbbfcff56d9ba3ceeba371dd3057e58f58e8bdf4 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sun, 2 Aug 2026 00:49:19 -0700 Subject: [PATCH] fix(secrets): route authz gate and pairing allowlist reads through the profile secret scope - gateway/authz_mixin.py: group chat allowlists, {PLATFORM}_ALLOW_BOTS, and pairing-mode allowlist presence checks now go through the file's own _platform_gate_env helper (scoped-authoritative under multiplex). - gateway/pairing.py: grant-mirror/revoke allowlist READS go through get_secret (Slack pattern for unscoped admin/CLI callers); writes still use save_env_value with a TODO for profile-aware writes. --- gateway/authz_mixin.py | 10 +++++----- gateway/pairing.py | 30 ++++++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/gateway/authz_mixin.py b/gateway/authz_mixin.py index f1bb24bb8a..144cb50431 100644 --- a/gateway/authz_mixin.py +++ b/gateway/authz_mixin.py @@ -456,7 +456,7 @@ class GatewayAuthorizationMixin: Platform.QQBOT: "QQ_GROUP_ALLOWED_USERS", }.get(source.platform, "") if chat_allowlist_env: - raw_chat_allowlist = os.getenv(chat_allowlist_env, "").strip() + raw_chat_allowlist = _platform_gate_env(chat_allowlist_env) if raw_chat_allowlist: allowed_group_ids = { cid.strip() @@ -498,7 +498,7 @@ class GatewayAuthorizationMixin: } if getattr(source, "is_bot", False): allow_bots_var = platform_allow_bots_map.get(source.platform) - if allow_bots_var and os.getenv(allow_bots_var, "none").lower().strip() in {"mentions", "all"}: + if allow_bots_var and _platform_gate_env(allow_bots_var, "none").lower().strip() in {"mentions", "all"}: return True if not user_id: @@ -876,13 +876,13 @@ class GatewayAuthorizationMixin: ), Platform.QQBOT: ("QQ_GROUP_ALLOWED_USERS",), } - if os.getenv(platform_env_map.get(platform, ""), "").strip(): + if _platform_gate_env(platform_env_map.get(platform, "")).strip(): return "ignore" for env_key in platform_group_env_map.get(platform, ()): - if os.getenv(env_key, "").strip(): + if _platform_gate_env(env_key).strip(): return "ignore" - if os.getenv("GATEWAY_ALLOWED_USERS", "").strip(): + if _platform_gate_env("GATEWAY_ALLOWED_USERS").strip(): return "ignore" return "pair" diff --git a/gateway/pairing.py b/gateway/pairing.py index 4e4ec14f39..42ce7a89e5 100644 --- a/gateway/pairing.py +++ b/gateway/pairing.py @@ -146,6 +146,32 @@ def _user_ids_match(platform: str, left: str, right: str) -> bool: return bool(left_aliases and right_aliases and (left_aliases & right_aliases)) +def _read_allowlist_env(env_var: str) -> str: + """Read a platform allowlist env var through the profile secret scope. + + Under multiplexing the process env may hold ANOTHER profile's allowlist + (first-writer-wins YAML→env bridges), so reads must honor the installed + scope's verdict — including a scoped miss returning empty rather than + borrowing the process value. Unscoped callers (single-profile CLI / + admin endpoints) keep the legacy ``os.getenv`` read. + + TODO(profile-secrets): the grant mirror below still WRITES through + ``hermes_cli.config.save_env_value`` / ``remove_env_value``, which target + the root ``.env`` — those writes need a profile-aware counterpart before + pairing grants can be mirrored correctly under multiplexing. + """ + try: + from agent.secret_scope import UnscopedSecretError, get_secret + + try: + return (get_secret(env_var) or "").strip() + except UnscopedSecretError: + pass + except Exception: + pass + return (os.getenv(env_var) or "").strip() + + def _sync_allowlist_add(platform: str, user_id: str) -> None: """Add ``user_id`` to the platform allowlist env var IF one is configured. @@ -158,7 +184,7 @@ def _sync_allowlist_add(platform: str, user_id: str) -> None: env_var = _allowlist_env_for_platform(platform) if not env_var: return - current = os.getenv(env_var, "").strip() + current = _read_allowlist_env(env_var) if not current: return # No allowlist configured — leave the gateway open (option i). ids = _split_allowlist(current) @@ -278,7 +304,7 @@ def _sync_allowlist_remove(platform: str, user_id: str) -> None: env_var = _allowlist_env_for_platform(platform) if not env_var: return - current = os.getenv(env_var, "").strip() + current = _read_allowlist_env(env_var) if not current: return # No allowlist configured — do not touch config-only snapshots. ids = _split_allowlist(current)