fix(secrets): scope-aware credential reads in core tool/gateway/web-server paths
TOOL_GATEWAY_USER_TOKEN (managed_tool_gateway), OPENROUTER_API_KEY presence (openrouter_client), SUDO_PASSWORD (terminal_tool), GATEWAY_PROXY_KEY (gateway/run), SLACK_BOT_TOKEN presence (gateway/session), and the ELEVENLABS_API_KEY env fallback (web_server voices endpoint) now honor the installed profile secret scope; unscoped callers keep legacy env reads via the UnscopedSecretError fallback (Slack pattern).
This commit is contained in:
parent
359ff01c23
commit
5c6cc38010
|
|
@ -23074,7 +23074,18 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
"tools": [],
|
||||
}
|
||||
|
||||
proxy_key = os.getenv("GATEWAY_PROXY_KEY", "").strip()
|
||||
# Scope-aware read: the proxy key is a per-profile credential; under
|
||||
# multiplex honor the installed scope's verdict (Slack pattern for
|
||||
# the unscoped default-profile loop).
|
||||
try:
|
||||
from agent.secret_scope import UnscopedSecretError, get_secret
|
||||
|
||||
try:
|
||||
proxy_key = (get_secret("GATEWAY_PROXY_KEY") or "").strip()
|
||||
except UnscopedSecretError:
|
||||
proxy_key = os.getenv("GATEWAY_PROXY_KEY", "").strip()
|
||||
except Exception:
|
||||
proxy_key = os.getenv("GATEWAY_PROXY_KEY", "").strip()
|
||||
|
||||
def _run_still_current() -> bool:
|
||||
if run_generation is None or not session_key:
|
||||
|
|
|
|||
|
|
@ -387,7 +387,19 @@ def _slack_tools_loaded() -> bool:
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
if not (os.environ.get("SLACK_BOT_TOKEN") or "").strip():
|
||||
# Presence check through the profile secret scope: under multiplex the
|
||||
# process env may carry another profile's token (Slack pattern for the
|
||||
# unscoped default-profile path).
|
||||
try:
|
||||
from agent.secret_scope import UnscopedSecretError, get_secret
|
||||
|
||||
try:
|
||||
_slack_token = get_secret("SLACK_BOT_TOKEN") or ""
|
||||
except UnscopedSecretError:
|
||||
_slack_token = os.environ.get("SLACK_BOT_TOKEN") or ""
|
||||
except Exception:
|
||||
_slack_token = os.environ.get("SLACK_BOT_TOKEN") or ""
|
||||
if not _slack_token.strip():
|
||||
return False
|
||||
try:
|
||||
from hermes_cli.config import load_config
|
||||
|
|
|
|||
|
|
@ -4320,7 +4320,20 @@ async def get_elevenlabs_voices(profile: Optional[str] = None):
|
|||
# Config-only scope (await-safe): the key lookup reads the requested
|
||||
# profile's .env, matching the profile the settings UI writes to.
|
||||
with _config_profile_scope(profile):
|
||||
api_key = (load_env().get("ELEVENLABS_API_KEY") or os.environ.get("ELEVENLABS_API_KEY") or "").strip()
|
||||
api_key = (load_env().get("ELEVENLABS_API_KEY") or "").strip()
|
||||
if not api_key:
|
||||
# Fallback for env-only deployments — scope-aware (Slack pattern):
|
||||
# under multiplex os.environ may hold another profile's key, so
|
||||
# honor the installed scope's verdict before touching the env.
|
||||
try:
|
||||
from agent.secret_scope import UnscopedSecretError, get_secret
|
||||
|
||||
try:
|
||||
api_key = (get_secret("ELEVENLABS_API_KEY") or "").strip()
|
||||
except UnscopedSecretError:
|
||||
api_key = (os.environ.get("ELEVENLABS_API_KEY") or "").strip()
|
||||
except Exception:
|
||||
api_key = (os.environ.get("ELEVENLABS_API_KEY") or "").strip()
|
||||
if not api_key:
|
||||
return {"available": False, "voices": []}
|
||||
|
||||
|
|
|
|||
|
|
@ -73,6 +73,28 @@ def _access_token_is_expiring(expires_at: object, skew_seconds: int) -> bool:
|
|||
return remaining <= max(0, int(skew_seconds))
|
||||
|
||||
|
||||
def _read_user_token_override() -> Optional[str]:
|
||||
"""Read the TOOL_GATEWAY_USER_TOKEN env override through the secret scope.
|
||||
|
||||
Availability scans run both inside agent turns (scope installed) and in
|
||||
unscoped CLI paths, so this uses the Slack pattern: honor the scope's
|
||||
verdict when installed (a scoped miss does NOT borrow the process env
|
||||
under multiplex), fall back to ``os.environ`` only when unscoped.
|
||||
"""
|
||||
try:
|
||||
from agent.secret_scope import UnscopedSecretError, get_secret
|
||||
|
||||
try:
|
||||
explicit = get_secret("TOOL_GATEWAY_USER_TOKEN")
|
||||
except UnscopedSecretError:
|
||||
explicit = os.getenv("TOOL_GATEWAY_USER_TOKEN")
|
||||
except Exception:
|
||||
explicit = os.getenv("TOOL_GATEWAY_USER_TOKEN")
|
||||
if isinstance(explicit, str) and explicit.strip():
|
||||
return explicit.strip()
|
||||
return None
|
||||
|
||||
|
||||
def peek_nous_access_token() -> Optional[str]:
|
||||
"""Cheap probe for a Nous gateway token without triggering refresh.
|
||||
|
||||
|
|
@ -83,9 +105,9 @@ def peek_nous_access_token() -> Optional[str]:
|
|||
network calls. Truthful refresh handling stays in request/session paths
|
||||
that call :func:`read_nous_access_token`.
|
||||
"""
|
||||
explicit = os.getenv("TOOL_GATEWAY_USER_TOKEN")
|
||||
if isinstance(explicit, str) and explicit.strip():
|
||||
return explicit.strip()
|
||||
explicit = _read_user_token_override()
|
||||
if explicit:
|
||||
return explicit
|
||||
|
||||
nous_provider = _read_nous_provider_state() or {}
|
||||
access_token = nous_provider.get("access_token")
|
||||
|
|
@ -96,9 +118,9 @@ def peek_nous_access_token() -> Optional[str]:
|
|||
|
||||
def read_nous_access_token() -> Optional[str]:
|
||||
"""Read a Nous Subscriber OAuth access token from auth store or env override."""
|
||||
explicit = os.getenv("TOOL_GATEWAY_USER_TOKEN")
|
||||
if isinstance(explicit, str) and explicit.strip():
|
||||
return explicit.strip()
|
||||
explicit = _read_user_token_override()
|
||||
if explicit:
|
||||
return explicit
|
||||
nous_provider = _read_nous_provider_state() or {}
|
||||
cached_token = peek_nous_access_token()
|
||||
|
||||
|
|
|
|||
|
|
@ -29,5 +29,19 @@ def get_async_client():
|
|||
|
||||
|
||||
def check_api_key() -> bool:
|
||||
"""Check whether the OpenRouter API key is present."""
|
||||
"""Check whether the OpenRouter API key is present.
|
||||
|
||||
Scope-aware (Slack pattern): tool paths run inside an installed profile
|
||||
secret scope, whose verdict is authoritative under multiplex; unscoped
|
||||
CLI probes keep the legacy env read.
|
||||
"""
|
||||
try:
|
||||
from agent.secret_scope import UnscopedSecretError, get_secret
|
||||
|
||||
try:
|
||||
return bool(get_secret("OPENROUTER_API_KEY"))
|
||||
except UnscopedSecretError:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
return bool(os.getenv("OPENROUTER_API_KEY"))
|
||||
|
|
|
|||
|
|
@ -977,9 +977,21 @@ def _transform_sudo_command(command: str | None) -> tuple[str | None, str | None
|
|||
if sudo_count == 0:
|
||||
return command, None
|
||||
|
||||
has_configured_password = "SUDO_PASSWORD" in os.environ
|
||||
# Scope-aware read (Slack pattern): under multiplex the process env may
|
||||
# hold another profile's SUDO_PASSWORD, so honor the installed scope's
|
||||
# verdict; unscoped callers keep the legacy os.environ read.
|
||||
try:
|
||||
from agent.secret_scope import UnscopedSecretError, get_secret
|
||||
|
||||
try:
|
||||
_configured_password = get_secret("SUDO_PASSWORD")
|
||||
except UnscopedSecretError:
|
||||
_configured_password = os.environ.get("SUDO_PASSWORD")
|
||||
except Exception:
|
||||
_configured_password = os.environ.get("SUDO_PASSWORD")
|
||||
has_configured_password = _configured_password is not None
|
||||
sudo_password = (
|
||||
os.environ.get("SUDO_PASSWORD", "")
|
||||
_configured_password
|
||||
if has_configured_password
|
||||
else _get_cached_sudo_password()
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in New Issue