From f4b268b7852694abc8207cbf5dacebcc1cff9942 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sun, 2 Aug 2026 00:53:55 -0700 Subject: [PATCH] =?UTF-8?q?fix(secrets):=20Slack-pattern=20scoped=20creden?= =?UTF-8?q?tial=20reads=20=E2=80=94=20Feishu,=20WeCom,=20Photon,=20Buzz?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FEISHU_APP_SECRET/FEISHU_ENCRYPT_KEY/FEISHU_VERIFICATION_TOKEN, WECOM_SECRET, PHOTON_PROJECT_SECRET/PHOTON_SIDECAR_TOKEN (adapter + auth.load_project_credentials) and BUZZ_PRIVATE_KEY now read through _get_scoped_secret. --- plugins/platforms/buzz/adapter.py | 26 ++++++++++++++++++++++- plugins/platforms/feishu/adapter.py | 30 ++++++++++++++++++++++++--- plugins/platforms/photon/adapter.py | 32 +++++++++++++++++++++++++---- plugins/platforms/photon/auth.py | 26 ++++++++++++++++++++++- plugins/platforms/wecom/adapter.py | 26 ++++++++++++++++++++++- 5 files changed, 130 insertions(+), 10 deletions(-) diff --git a/plugins/platforms/buzz/adapter.py b/plugins/platforms/buzz/adapter.py index b07830ae9b..8b77ae3341 100644 --- a/plugins/platforms/buzz/adapter.py +++ b/plugins/platforms/buzz/adapter.py @@ -49,6 +49,30 @@ from pathlib import Path from typing import Any, Dict, List, Optional, Tuple from urllib.parse import urlsplit, urlunsplit +from agent.secret_scope import UnscopedSecretError as _UnscopedSecretError +from agent.secret_scope import get_secret as _scoped_get_secret + + +def _get_scoped_secret(name, default=None): + """Scope-aware credential read with the default-profile startup fallback. + + Secondary profiles construct their adapters under a profile secret + scope -- the scope is authoritative and a scoped miss returns ``default`` + (no cross-profile borrow from ``os.environ``, which may hold another + profile's value). The DEFAULT profile's adapter constructs and sends + *unscoped* under multiplexing, where a bare ``get_secret`` would raise + ``UnscopedSecretError`` and crash this path; there ``os.environ`` is that + profile's own value, so fall back to it. Same pattern as the Slack + ``SLACK_APP_TOKEN`` read (#59739) and + ``gateway/platforms/whatsapp_common.py::_get_wsecret``. + """ + try: + val = _scoped_get_secret(name, default) + except _UnscopedSecretError: + val = os.getenv(name) + return val if val is not None else default + + logger = logging.getLogger(__name__) from gateway.platforms.base import ( @@ -226,7 +250,7 @@ def _resolve_private_key(extra: Optional[dict] = None) -> str: NEVER log the return value. """ - key = os.getenv("BUZZ_PRIVATE_KEY", "").strip() + key = _get_scoped_secret("BUZZ_PRIVATE_KEY", "").strip() if key: return key configured = os.getenv("BUZZ_CREDENTIALS_FILE", "").strip() or (extra or {}).get("credentials_file", "") diff --git a/plugins/platforms/feishu/adapter.py b/plugins/platforms/feishu/adapter.py index d86dc597a8..c21bb42e90 100644 --- a/plugins/platforms/feishu/adapter.py +++ b/plugins/platforms/feishu/adapter.py @@ -145,6 +145,30 @@ from gateway.status import acquire_scoped_lock, release_scoped_lock from hermes_constants import get_hermes_home from utils import atomic_json_write, env_float, env_int +from agent.secret_scope import UnscopedSecretError as _UnscopedSecretError +from agent.secret_scope import get_secret as _scoped_get_secret + + +def _get_scoped_secret(name, default=None): + """Scope-aware credential read with the default-profile startup fallback. + + Secondary profiles construct their adapters under a profile secret + scope -- the scope is authoritative and a scoped miss returns ``default`` + (no cross-profile borrow from ``os.environ``, which may hold another + profile's value). The DEFAULT profile's adapter constructs and sends + *unscoped* under multiplexing, where a bare ``get_secret`` would raise + ``UnscopedSecretError`` and crash this path; there ``os.environ`` is that + profile's own value, so fall back to it. Same pattern as the Slack + ``SLACK_APP_TOKEN`` read (#59739) and + ``gateway/platforms/whatsapp_common.py::_get_wsecret``. + """ + try: + val = _scoped_get_secret(name, default) + except _UnscopedSecretError: + val = os.getenv(name) + return val if val is not None else default + + logger = logging.getLogger(__name__) # --------------------------------------------------------------------------- @@ -1553,14 +1577,14 @@ class FeishuAdapter(BasePlatformAdapter): return FeishuAdapterSettings( app_id=str(extra.get("app_id") or os.getenv("FEISHU_APP_ID", "")).strip(), - app_secret=str(extra.get("app_secret") or os.getenv("FEISHU_APP_SECRET", "")).strip(), + app_secret=str(extra.get("app_secret") or _get_scoped_secret("FEISHU_APP_SECRET", "")).strip(), domain_name=str(extra.get("domain") or os.getenv("FEISHU_DOMAIN", "feishu")).strip().lower(), connection_mode=str( extra.get("connection_mode") or os.getenv("FEISHU_CONNECTION_MODE", "websocket") ).strip().lower(), - encrypt_key=str(extra.get("encrypt_key") or os.getenv("FEISHU_ENCRYPT_KEY", "")).strip(), + encrypt_key=str(extra.get("encrypt_key") or _get_scoped_secret("FEISHU_ENCRYPT_KEY", "")).strip(), verification_token=str( - extra.get("verification_token") or os.getenv("FEISHU_VERIFICATION_TOKEN", "") + extra.get("verification_token") or _get_scoped_secret("FEISHU_VERIFICATION_TOKEN", "") ).strip(), group_policy=os.getenv("FEISHU_GROUP_POLICY", "allowlist").strip().lower(), allowed_group_users=frozenset( diff --git a/plugins/platforms/photon/adapter.py b/plugins/platforms/photon/adapter.py index abebe3669f..d7f89c4a69 100644 --- a/plugins/platforms/photon/adapter.py +++ b/plugins/platforms/photon/adapter.py @@ -67,6 +67,30 @@ from gateway.platforms.helpers import compile_mention_patterns, strip_markdown from .auth import load_project_credentials +from agent.secret_scope import UnscopedSecretError as _UnscopedSecretError +from agent.secret_scope import get_secret as _scoped_get_secret + + +def _get_scoped_secret(name, default=None): + """Scope-aware credential read with the default-profile startup fallback. + + Secondary profiles construct their adapters under a profile secret + scope -- the scope is authoritative and a scoped miss returns ``default`` + (no cross-profile borrow from ``os.environ``, which may hold another + profile's value). The DEFAULT profile's adapter constructs and sends + *unscoped* under multiplexing, where a bare ``get_secret`` would raise + ``UnscopedSecretError`` and crash this path; there ``os.environ`` is that + profile's own value, so fall back to it. Same pattern as the Slack + ``SLACK_APP_TOKEN`` read (#59739) and + ``gateway/platforms/whatsapp_common.py::_get_wsecret``. + """ + try: + val = _scoped_get_secret(name, default) + except _UnscopedSecretError: + val = os.getenv(name) + return val if val is not None else default + + logger = logging.getLogger(__name__) # --------------------------------------------------------------------------- @@ -511,7 +535,7 @@ def _reinstall_sidecar_deps() -> None: def validate_config(cfg: PlatformConfig) -> bool: extra = cfg.extra or {} project_id = extra.get("project_id") or os.getenv("PHOTON_PROJECT_ID") - project_secret = extra.get("project_secret") or os.getenv("PHOTON_PROJECT_SECRET") + project_secret = extra.get("project_secret") or _get_scoped_secret("PHOTON_PROJECT_SECRET") if not project_id or not project_secret: # Fall back to auth.json stored_id, stored_sec = load_project_credentials() @@ -694,7 +718,7 @@ class PhotonAdapter(BasePlatformAdapter): or "" ) self._project_secret: str = ( - os.getenv("PHOTON_PROJECT_SECRET") + _get_scoped_secret("PHOTON_PROJECT_SECRET") or extra.get("project_secret") or stored_sec or "" @@ -707,7 +731,7 @@ class PhotonAdapter(BasePlatformAdapter): ) self._sidecar_bind = _DEFAULT_SIDECAR_BIND self._sidecar_token = ( - os.getenv("PHOTON_SIDECAR_TOKEN") or secrets.token_hex(16) + _get_scoped_secret("PHOTON_SIDECAR_TOKEN") or secrets.token_hex(16) ) self._autostart_sidecar = str( os.getenv("PHOTON_SIDECAR_AUTOSTART", "true") @@ -2730,7 +2754,7 @@ async def _standalone_send( (pconfig.extra or {}).get("sidecar_port") or os.getenv("PHOTON_SIDECAR_PORT"), _DEFAULT_SIDECAR_PORT, ) - token = os.getenv("PHOTON_SIDECAR_TOKEN") + token = _get_scoped_secret("PHOTON_SIDECAR_TOKEN") if not token: # Fall back to the runtime record the gateway persists once its # sidecar passes /healthz (issue #69960) — the token only exists in diff --git a/plugins/platforms/photon/auth.py b/plugins/platforms/photon/auth.py index b8b356a16b..34b573a2d8 100644 --- a/plugins/platforms/photon/auth.py +++ b/plugins/platforms/photon/auth.py @@ -53,6 +53,30 @@ try: except ImportError: # pragma: no cover - httpx is a hermes dependency httpx = None # type: ignore[assignment] +from agent.secret_scope import UnscopedSecretError as _UnscopedSecretError +from agent.secret_scope import get_secret as _scoped_get_secret + + +def _get_scoped_secret(name, default=None): + """Scope-aware credential read with the default-profile startup fallback. + + Secondary profiles construct their adapters under a profile secret + scope -- the scope is authoritative and a scoped miss returns ``default`` + (no cross-profile borrow from ``os.environ``, which may hold another + profile's value). The DEFAULT profile's adapter constructs and sends + *unscoped* under multiplexing, where a bare ``get_secret`` would raise + ``UnscopedSecretError`` and crash this path; there ``os.environ`` is that + profile's own value, so fall back to it. Same pattern as the Slack + ``SLACK_APP_TOKEN`` read (#59739) and + ``gateway/platforms/whatsapp_common.py::_get_wsecret``. + """ + try: + val = _scoped_get_secret(name, default) + except _UnscopedSecretError: + val = os.getenv(name) + return val if val is not None else default + + logger = logging.getLogger(__name__) @@ -231,7 +255,7 @@ def load_project_credentials() -> Tuple[Optional[str], Optional[str]]: is the unified project id (dashboard id == spectrumProjectId). """ env_id = os.getenv("PHOTON_PROJECT_ID") - env_sec = os.getenv("PHOTON_PROJECT_SECRET") + env_sec = _get_scoped_secret("PHOTON_PROJECT_SECRET") if env_id and env_sec: return env_id, env_sec auth = _load_auth() diff --git a/plugins/platforms/wecom/adapter.py b/plugins/platforms/wecom/adapter.py index 2f8e6d66fc..715445bd23 100644 --- a/plugins/platforms/wecom/adapter.py +++ b/plugins/platforms/wecom/adapter.py @@ -70,6 +70,30 @@ from gateway.platforms.base import ( ) from utils import env_float +from agent.secret_scope import UnscopedSecretError as _UnscopedSecretError +from agent.secret_scope import get_secret as _scoped_get_secret + + +def _get_scoped_secret(name, default=None): + """Scope-aware credential read with the default-profile startup fallback. + + Secondary profiles construct their adapters under a profile secret + scope -- the scope is authoritative and a scoped miss returns ``default`` + (no cross-profile borrow from ``os.environ``, which may hold another + profile's value). The DEFAULT profile's adapter constructs and sends + *unscoped* under multiplexing, where a bare ``get_secret`` would raise + ``UnscopedSecretError`` and crash this path; there ``os.environ`` is that + profile's own value, so fall back to it. Same pattern as the Slack + ``SLACK_APP_TOKEN`` read (#59739) and + ``gateway/platforms/whatsapp_common.py::_get_wsecret``. + """ + try: + val = _scoped_get_secret(name, default) + except _UnscopedSecretError: + val = os.getenv(name) + return val if val is not None else default + + logger = logging.getLogger(__name__) DEFAULT_WS_URL = "wss://openws.work.weixin.qq.com" @@ -154,7 +178,7 @@ class WeComAdapter(BasePlatformAdapter): extra = config.extra or {} self._bot_id = str(extra.get("bot_id") or os.getenv("WECOM_BOT_ID", "")).strip() - self._secret = str(extra.get("secret") or os.getenv("WECOM_SECRET", "")).strip() + self._secret = str(extra.get("secret") or _get_scoped_secret("WECOM_SECRET", "")).strip() self._ws_url = str( extra.get("websocket_url") or extra.get("websocketUrl")