fix(secrets): Slack-pattern scoped credential reads — Feishu, WeCom, Photon, Buzz
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.
This commit is contained in:
@@ -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", "")
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user