diff --git a/plugins/platforms/sms/adapter.py b/plugins/platforms/sms/adapter.py index d17d4e1191..43e555b3aa 100644 --- a/plugins/platforms/sms/adapter.py +++ b/plugins/platforms/sms/adapter.py @@ -90,7 +90,9 @@ class SmsAdapter(BasePlatformAdapter): super().__init__(config, Platform.SMS) self._account_sid: str = _get_scoped_secret("TWILIO_ACCOUNT_SID", "") self._auth_token: str = _get_scoped_secret("TWILIO_AUTH_TOKEN", "") - self._from_number: str = os.getenv("TWILIO_PHONE_NUMBER", "") + # Scoped like the sibling reads above: a secondary profile must not send from the default + # profile's TWILIO_PHONE_NUMBER (#98738 class). + self._from_number: str = _get_scoped_secret("TWILIO_PHONE_NUMBER", "") self._webhook_port: int = int(os.getenv("SMS_WEBHOOK_PORT", str(DEFAULT_WEBHOOK_PORT))) self._webhook_host: str = os.getenv("SMS_WEBHOOK_HOST", DEFAULT_WEBHOOK_HOST) self._webhook_url: str = os.getenv("SMS_WEBHOOK_URL", "").strip() @@ -297,7 +299,7 @@ async def _standalone_send(pconfig, chat_id, message, *, thread_id=None, media_f if not AIOHTTP_AVAILABLE: return {"error": "aiohttp not installed. Run: pip install aiohttp"} account_sid = _get_scoped_secret("TWILIO_ACCOUNT_SID", "") - from_number = os.getenv("TWILIO_PHONE_NUMBER", "") + from_number = _get_scoped_secret("TWILIO_PHONE_NUMBER", "") # scoped like account_sid: never the default's number if not account_sid or not auth_token or not from_number: return {"error": "SMS not configured (TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_PHONE_NUMBER required)"} message = _strip_markdown_for_sms(message) diff --git a/tests/gateway/test_sms.py b/tests/gateway/test_sms.py index 3598182ef6..c2a96777ad 100644 --- a/tests/gateway/test_sms.py +++ b/tests/gateway/test_sms.py @@ -321,3 +321,44 @@ class TestWebhookSignatureEnforcement: request = self._mock_request(oversized, content_length=None) resp = await adapter._handle_webhook(request) assert resp.status == 413 + + + +class TestMultiplexProfileScope: + """TWILIO_PHONE_NUMBER must resolve through the same profile scope as the Twilio secrets: under + multiplex, os.environ holds the DEFAULT profile's number.""" + + @pytest.fixture(autouse=True) + def _default_profile_env(self, monkeypatch): + from agent.secret_scope import set_multiplex_active + for key, value in (("TWILIO_ACCOUNT_SID", "AC-default"), ("TWILIO_AUTH_TOKEN", "token-default"), + ("TWILIO_PHONE_NUMBER", "+15550000000")): + monkeypatch.setenv(key, value) + set_multiplex_active(True) + yield + set_multiplex_active(False) + + def test_init_pairs_secondary_secrets_with_secondary_from_number(self): + from agent.secret_scope import reset_secret_scope, set_secret_scope + from plugins.platforms.sms.adapter import SmsAdapter + + token = set_secret_scope({"TWILIO_ACCOUNT_SID": "AC-profile", "TWILIO_AUTH_TOKEN": "token-profile", + "TWILIO_PHONE_NUMBER": "+15551112222"}) + try: + adapter = SmsAdapter(PlatformConfig(enabled=True)) + finally: + reset_secret_scope(token) + assert (adapter._account_sid, adapter._from_number) == ("AC-profile", "+15551112222") + + @pytest.mark.asyncio + async def test_standalone_send_without_own_number_fails_closed(self): + """A secondary lacking its own from-number must NOT send from the default's +15550000000.""" + from agent.secret_scope import reset_secret_scope, set_secret_scope + from plugins.platforms.sms.adapter import _standalone_send + + token = set_secret_scope({"TWILIO_ACCOUNT_SID": "AC-profile", "TWILIO_AUTH_TOKEN": "token-profile"}) + try: + result = await _standalone_send(PlatformConfig(enabled=True), "+15559998888", "hi") + finally: + reset_secret_scope(token) + assert "TWILIO_PHONE_NUMBER required" in result["error"]