fix(simplex): scope SIMPLEX_* reads to the active profile under multiplexing

SimplexAdapter.__init__ (auto_accept, group_allowed), the registry gates
check_requirements/validate_config/is_connected, _env_enablement and
_standalone_send all read SIMPLEX_* via raw os.getenv. Under
gateway.multiplex_profiles those paths run inside a secondary profile's
scope where os.environ holds the DEFAULT profile's YAML-to-env bridge
output -- so a secondary profile that never configured SimpleX was
auto-enabled on the default's daemon URL and inherited its group
allowlist / auto-accept setting.

Route every read through the module-local `_get_scoped_secret` wrapper
(get_secret; UnscopedSecretError -> os.getenv for the default profile,
which constructs unscoped) -- the same helper the IRC/ntfy/Photon/
Mattermost siblings use. Unlike the extra-only `_scoped_platform_setting`
shape proposed in #100241, this honors BOTH the secondary profile's own
.env (the scope) and its config.yaml extra, and needs no config.yaml
re-read in check_requirements.

Rewrite of #100241.

Co-authored-by: nftpoetrist <264138787+nftpoetrist@users.noreply.github.com>
This commit is contained in:
Teknium
2026-09-02 03:35:18 -07:00
parent 07ee457a21
commit 2bcbdb61a7
2 changed files with 103 additions and 11 deletions

View File

@@ -56,6 +56,28 @@ from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional
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 env 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 holds the DEFAULT
profile's YAML-to-env bridge output under multiplexing). The default
profile's adapter constructs *unscoped*, where a bare ``get_secret``
would raise ``UnscopedSecretError``; there ``os.environ`` is that
profile's own value, so fall back to it. Same helper as the IRC/ntfy/
Mattermost plugins.
"""
try:
val = _scoped_get_secret(name, default)
except _UnscopedSecretError:
val = os.getenv(name)
return val if val is not None else default
# Lazy import: BasePlatformAdapter and friends live in the main repo.
# Imported at module top because they're stdlib-only inside Hermes — no
# external dependency that would block the plugin from loading.
@@ -153,7 +175,7 @@ class SimplexAdapter(BasePlatformAdapter):
# Contact-request auto-accept (on by default — matches the way most
# bot deployments expect to behave). Read from env first, then fall
# back to the value seeded by ``_env_enablement``.
env_auto = os.getenv("SIMPLEX_AUTO_ACCEPT")
env_auto = _get_scoped_secret("SIMPLEX_AUTO_ACCEPT")
if env_auto is not None:
self.auto_accept = env_auto.strip().lower() not in {"0", "false", "no", ""}
else:
@@ -162,7 +184,7 @@ class SimplexAdapter(BasePlatformAdapter):
# Group allowlist. Without ``SIMPLEX_GROUP_ALLOWED``, group messages
# are ignored entirely (safer default — a bot in a group otherwise
# processes every member's traffic). Use ``*`` to accept any group.
group_allowed_str = os.getenv("SIMPLEX_GROUP_ALLOWED", "") or extra.get(
group_allowed_str = _get_scoped_secret("SIMPLEX_GROUP_ALLOWED", "") or extra.get(
"group_allowed", ""
)
self.group_allow_from = set(_parse_comma_list(group_allowed_str))
@@ -1172,7 +1194,7 @@ def check_requirements() -> bool:
so the gateway never instantiates the adapter when the dependency is
missing or no daemon URL is configured.
"""
if not os.getenv("SIMPLEX_WS_URL"):
if not _get_scoped_secret("SIMPLEX_WS_URL"):
return False
try:
import websockets # noqa: F401
@@ -1184,14 +1206,14 @@ def check_requirements() -> bool:
def validate_config(config) -> bool:
"""Validate that the platform config has enough info to connect."""
extra = getattr(config, "extra", {}) or {}
ws_url = os.getenv("SIMPLEX_WS_URL") or extra.get("ws_url", "")
ws_url = _get_scoped_secret("SIMPLEX_WS_URL") or extra.get("ws_url", "")
return bool(ws_url)
def is_connected(config) -> bool:
"""Check whether SimpleX is configured (env or config.yaml)."""
extra = getattr(config, "extra", {}) or {}
ws_url = os.getenv("SIMPLEX_WS_URL") or extra.get("ws_url", "")
ws_url = _get_scoped_secret("SIMPLEX_WS_URL") or extra.get("ws_url", "")
return bool(ws_url)
@@ -1207,24 +1229,24 @@ def _env_enablement() -> Optional[dict]:
becomes a proper ``HomeChannel`` dataclass on the ``PlatformConfig``
rather than being merged into ``extra``.
"""
ws_url = os.getenv("SIMPLEX_WS_URL", "").strip()
ws_url = _get_scoped_secret("SIMPLEX_WS_URL", "").strip()
if not ws_url:
return None
seed: dict = {"ws_url": ws_url}
auto_accept = os.getenv("SIMPLEX_AUTO_ACCEPT", "").strip().lower()
auto_accept = _get_scoped_secret("SIMPLEX_AUTO_ACCEPT", "").strip().lower()
if auto_accept:
seed["auto_accept"] = auto_accept not in {"0", "false", "no"}
group_allowed = os.getenv("SIMPLEX_GROUP_ALLOWED", "").strip()
group_allowed = _get_scoped_secret("SIMPLEX_GROUP_ALLOWED", "").strip()
if group_allowed:
seed["group_allowed"] = group_allowed
home = os.getenv("SIMPLEX_HOME_CHANNEL", "").strip()
home = _get_scoped_secret("SIMPLEX_HOME_CHANNEL", "").strip()
if home:
seed["home_channel"] = {
"chat_id": home,
"name": os.getenv("SIMPLEX_HOME_CHANNEL_NAME", "").strip() or home,
"name": _get_scoped_secret("SIMPLEX_HOME_CHANNEL_NAME", "").strip() or home,
}
return seed
@@ -1257,7 +1279,7 @@ async def _standalone_send(
return {"error": "websockets not installed. Run: pip install websockets"}
extra = getattr(pconfig, "extra", {}) or {}
ws_url = os.getenv("SIMPLEX_WS_URL") or extra.get(
ws_url = _get_scoped_secret("SIMPLEX_WS_URL") or extra.get(
"ws_url", "ws://127.0.0.1:5225"
)
if not ws_url:

View File

@@ -388,3 +388,73 @@ def _make_file_chat_item(file_path: str, file_name: str) -> dict:
}
# ---------------------------------------------------------------------------
# Multiplex secondary-profile scope
# ---------------------------------------------------------------------------
#
# Every SIMPLEX_* read (auto_accept / group_allowed in __init__, ws_url in the
# registry gates, everything in _env_enablement) went through raw os.getenv,
# which under multiplexing holds the DEFAULT profile's YAML-to-env bridge
# output -- a secondary profile silently borrowed the default's daemon URL,
# group allowlist and auto-accept setting. Reads now go through the module's
# ``_get_scoped_secret`` (profile .env AND extra both honored; scoped miss
# fails closed; unscoped default profile keeps env precedence).
@pytest.fixture
def multiplex_scope():
"""Install multiplex + a secondary-profile secret scope; restore after."""
from agent.secret_scope import (
reset_secret_scope,
set_multiplex_active,
set_secret_scope,
)
tokens = []
def install(scope=None):
set_multiplex_active(True)
tokens.append(set_secret_scope(scope or {}))
yield install
for token in reversed(tokens):
reset_secret_scope(token)
set_multiplex_active(False)
@pytest.fixture
def default_profile_env(monkeypatch):
"""The default profile's YAML-to-env bridge output in os.environ."""
monkeypatch.setenv("SIMPLEX_WS_URL", "ws://default:5225")
monkeypatch.setenv("SIMPLEX_GROUP_ALLOWED", "*")
monkeypatch.setenv("SIMPLEX_AUTO_ACCEPT", "true")
def test_multiplex_scoped_miss_does_not_borrow_default_profile_env(
multiplex_scope, default_profile_env
):
"""A secondary profile with no SimpleX config of its own must not be
auto-enabled off the default's daemon URL, nor inherit its wide-open
group allowlist."""
from gateway.config import PlatformConfig
multiplex_scope({"SOMETHING_ELSE": "x"})
assert _env_enablement() is None
assert check_requirements() is False
assert is_connected(PlatformConfig(enabled=True, extra={})) is False
adapter = SimplexAdapter(PlatformConfig(enabled=True, extra={"auto_accept": False}))
assert adapter.group_allow_from == set()
assert adapter.auto_accept is False
def test_multiplex_scope_reads_profile_own_env_not_default(
multiplex_scope, default_profile_env
):
"""A secondary profile's own .env (installed as the scope) is honored --
the extra-only shape would have ignored it."""
multiplex_scope({"SIMPLEX_WS_URL": "ws://profile:5225", "SIMPLEX_GROUP_ALLOWED": "g1"})
seeded = _env_enablement()
assert seeded == {"ws_url": "ws://profile:5225", "group_allowed": "g1"}
assert check_requirements() is True