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:
Teknium 2026-08-02 00:52:55 -07:00
parent 359ff01c23
commit 5c6cc38010
6 changed files with 96 additions and 12 deletions

View File

@ -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:

View File

@ -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

View File

@ -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": []}

View File

@ -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()

View File

@ -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"))

View File

@ -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()
)