diff --git a/src/agentex/lib/core/clients/temporal/auth.py b/src/agentex/lib/core/clients/temporal/auth.py index 09886d8d4..7c757b1e4 100644 --- a/src/agentex/lib/core/clients/temporal/auth.py +++ b/src/agentex/lib/core/clients/temporal/auth.py @@ -17,6 +17,7 @@ logger = logging.getLogger(__name__) _AUTH_SCOPE_ENV = "TEMPORAL_AUTH_SCOPE" +_NAMESPACE_ENV = "TEMPORAL_NAMESPACE" _REFRESH_MARGIN_SECONDS = 300 _MIN_REFRESH_DELAY_SECONDS = 30 _RETRY_DELAY_SECONDS = 30 @@ -85,6 +86,10 @@ async def _run(self) -> None: async def connect_temporal_client(connect_options: dict[str, Any]) -> Client: """Connect anonymously by default, or with renewable Workload Identity.""" + namespace = os.environ.get(_NAMESPACE_ENV) + if namespace and "namespace" not in connect_options: + connect_options = {**connect_options, "namespace": namespace} + scope = os.environ.get(_AUTH_SCOPE_ENV) if not scope: return await Client.connect(**connect_options) diff --git a/tests/lib/test_temporal_auth.py b/tests/lib/test_temporal_auth.py index 59c7e1bd7..c04c27c52 100644 --- a/tests/lib/test_temporal_auth.py +++ b/tests/lib/test_temporal_auth.py @@ -47,6 +47,11 @@ async def close(self): self.closed = True +@pytest.fixture(autouse=True) +def _clear_temporal_namespace(monkeypatch): + monkeypatch.delenv("TEMPORAL_NAMESPACE", raising=False) + + async def test_connect_without_scope_preserves_legacy_path(monkeypatch): monkeypatch.delenv("TEMPORAL_AUTH_SCOPE", raising=False) client = MagicMock() @@ -63,6 +68,42 @@ async def test_connect_without_scope_preserves_legacy_path(monkeypatch): connect.assert_awaited_once_with(target_host="temporal:7233") +@pytest.mark.parametrize("scope", [None, _SCOPE]) +async def test_connect_uses_namespace_from_environment(monkeypatch, scope): + if scope is None: + monkeypatch.delenv("TEMPORAL_AUTH_SCOPE", raising=False) + else: + monkeypatch.setenv("TEMPORAL_AUTH_SCOPE", scope) + monkeypatch.setenv("TEMPORAL_NAMESPACE", "tenant-a") + + with patch( + "agentex.lib.core.clients.temporal.auth.WorkloadIdentityCredential", + return_value=FakeCredential(AccessToken(_INITIAL, 2_000)), + ): + with patch( + "agentex.lib.core.clients.temporal.auth.Client.connect", + new=AsyncMock(return_value=MagicMock()), + ) as connect: + with patch("agentex.lib.core.clients.temporal.auth.time.time", return_value=1_000): + with patch.object(TemporalTokenSession, "start"): + await connect_temporal_client({"target_host": "temporal:7233"}) + + assert connect.await_args.kwargs["namespace"] == "tenant-a" + + +async def test_connect_keeps_explicit_namespace(monkeypatch): + monkeypatch.delenv("TEMPORAL_AUTH_SCOPE", raising=False) + monkeypatch.setenv("TEMPORAL_NAMESPACE", "tenant-a") + + with patch( + "agentex.lib.core.clients.temporal.auth.Client.connect", + new=AsyncMock(return_value=MagicMock()), + ) as connect: + await connect_temporal_client({"target_host": "temporal:7233", "namespace": "tenant-b"}) + + connect.assert_awaited_once_with(target_host="temporal:7233", namespace="tenant-b") + + async def test_connect_initial_token_failure_blocks_startup(monkeypatch): monkeypatch.setenv("TEMPORAL_AUTH_SCOPE", _SCOPE) credential = FakeCredential(RuntimeError("identity unavailable"))