Skip to content

feat(temporal): enable TLS for all Temporal Cloud connections#12

Merged
TAOGenna merged 7 commits intomainfrom
feat/api-key-tls-support
Feb 18, 2026
Merged

feat(temporal): enable TLS for all Temporal Cloud connections#12
TAOGenna merged 7 commits intomainfrom
feat/api-key-tls-support

Conversation

@AngelGiampierre
Copy link
Contributor

@AngelGiampierre AngelGiampierre commented Feb 17, 2026

Temporal Cloud requires TLS on all connections. Previously, TLS was only configured when mTLS client certificates were provided, causing API key auth without certs to fail. Now TLS is enabled automatically for any Temporal Cloud connection (detected via is_cloud), regardless of the authentication method.

Additional troubleshooting of PR tests:

Root cause: Commit f9d6c2c (Feb 11) added if config.temporal.enabled: guards
to dev() and serve() in cli.py, but didn't update the tests. The CLI tests
called dev(config_file=None), which loaded .olive.yaml from the repo root
(all defaults → temporal.enabled=False). With temporal disabled, the code
skipped the temporal check block and hit an unmocked uvicorn.run(), which
started a real server that blocked forever on Linux CI.

Why it didn't hang locally: On macOS, uvicorn.run() happened to fail
immediately with SystemExit(1) (port/reload issues), so the tests reported a
quick failure instead of hanging.

Fixes applied:

  1. Mock OliveConfig.from_file in all CLI tests to return a config with
    temporal.enabled=True
  2. Add timeout-minutes: 10 to the workflow
  3. Fix stale version assertion in test_init.py
  4. Add -p no:anyio -p no:langsmith and LANGSMITH_TRACING=false as CI
    hardening

Summary by CodeRabbit

  • New Features

    • Enforces TLS automatically for Temporal Cloud when not explicitly configured.
    • Adds API-key (Bearer token) authentication support for Temporal Cloud.
    • Propagates Temporal Cloud namespace in request metadata when using cloud API keys.
  • Tests / CI

    • Adds test job timeout, adjusts pytest flags, and simplifies coverage output.
    • Updates tests to use a temporal-enabled test config and adds subprocess timeouts; updates expected package version.
  • Chores

    • Ignores local Temporal dev server database files.

Temporal Cloud requires TLS on all connections. Previously, TLS was only configured when mTLS client certificates were provided, causing API key auth without certs to fail. Now TLS is enabled automatically for any Temporal Cloud connection (detected via is_cloud), regardless of the authentication method.
@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

📝 Walkthrough

Walkthrough

Enables TLS by default for Temporal Cloud when unset, adds API-key authentication by injecting a Bearer token into rpc_metadata and propagates cloud_namespace as temporal-namespace; preserves existing client-cert and non-cloud behaviors.

Changes

Cohort / File(s) Summary
Temporal Cloud authentication & TLS
olive/temporal/worker.py
Enables TLS automatically for Temporal Cloud when not explicitly set; injects Authorization: Bearer <cloud_api_key> into rpc_metadata when cloud_api_key is present; forwards cloud_namespace as temporal-namespace in rpc_metadata; retains client-cert and non-cloud paths.
CI workflow
.github/workflows/tests.yml
Adds a 10-minute timeout to the test job; sets LANGSMITH_TRACING=false for tests; updates pytest invocation to -p no:anyio -p no:langsmith -v; adjusts coverage outputs (removes HTML report).
Ignore Temporal dev DB
.gitignore
Adds .temporal.db to ignore list (commented as Temporal dev server database).
Tests: CLI and init
tests/test_cli.py, tests/test_init.py
tests/test_cli.py: introduces TemporalConfig usage, adds helper to enable Temporal in mocked config, patches OliveConfig.from_file across tests, and adds timeout to subprocess.run calls; tests/test_init.py: updates expected package version from 1.3.2 to 1.4.2.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I nibble bytes and guard the gate,
TLS snug for cloud-bound fate,
A token tucked beneath my paw,
Namespaces echoed — neat and raw,
Hops and hops, secure and great!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately describes the main change: enabling TLS for Temporal Cloud connections. The title is concise, specific, and clearly summarizes the primary objective reflected across the code changes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/api-key-tls-support

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
olive/temporal/worker.py (1)

74-82: Use the SDK's built-in api_key parameter for Temporal Cloud authentication.

The temporalio Python SDK provides a dedicated api_key parameter on Client.connect() that automatically sets the Authorization: Bearer header. This is the officially documented pattern for Temporal Cloud and eliminates the need for manual rpc_metadata population.

♻️ Suggested refactor
         # Temporal Cloud API key auth
         if temporal_config.is_cloud and temporal_config.cloud_api_key:
-            connect_kwargs.setdefault("rpc_metadata", {})
-            connect_kwargs["rpc_metadata"]["authorization"] = f"Bearer {temporal_config.cloud_api_key}"
+            connect_kwargs["api_key"] = temporal_config.cloud_api_key
             if temporal_config.cloud_namespace:
                 connect_kwargs.setdefault("rpc_metadata", {})
-                connect_kwargs["rpc_metadata"].setdefault("temporal-namespace", temporal_config.cloud_namespace)
+                connect_kwargs["rpc_metadata"]["temporal-namespace"] = temporal_config.cloud_namespace
         elif temporal_config.namespace_endpoint:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@olive/temporal/worker.py` around lines 74 - 82, Replace the manual
rpc_metadata auth with the SDK api_key parameter: when using Temporal Cloud
(temporal_config.is_cloud and temporal_config.cloud_api_key) pass
temporal_config.cloud_api_key to Client.connect via the api_key argument instead
of setting connect_kwargs["rpc_metadata"]["authorization"]; likewise pass the
namespace via the Client.connect namespace argument
(temporal_config.cloud_namespace) rather than setting "temporal-namespace" in
rpc_metadata. For the non-cloud path that uses
temporal_config.namespace_endpoint/namespace, keep setting connect_kwargs but
ensure you populate the Client.connect namespace argument from
temporal_config.namespace instead of manually injecting "temporal-namespace"
into rpc_metadata. Use the existing temporal_config and connect_kwargs symbols
and update the code paths around Client.connect accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@olive/temporal/worker.py`:
- Around line 74-82: Replace the manual rpc_metadata auth with the SDK api_key
parameter: when using Temporal Cloud (temporal_config.is_cloud and
temporal_config.cloud_api_key) pass temporal_config.cloud_api_key to
Client.connect via the api_key argument instead of setting
connect_kwargs["rpc_metadata"]["authorization"]; likewise pass the namespace via
the Client.connect namespace argument (temporal_config.cloud_namespace) rather
than setting "temporal-namespace" in rpc_metadata. For the non-cloud path that
uses temporal_config.namespace_endpoint/namespace, keep setting connect_kwargs
but ensure you populate the Client.connect namespace argument from
temporal_config.namespace instead of manually injecting "temporal-namespace"
into rpc_metadata. Use the existing temporal_config and connect_kwargs symbols
and update the code paths around Client.connect accordingly.

TAOGenna
TAOGenna previously approved these changes Feb 17, 2026
Copy link
Collaborator

@TAOGenna TAOGenna left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

solves potential security issue.
LGTM

- Add timeout-minutes: 10 to prevent 6-hour zombie runs
- Disable anyio pytest plugin (-p no:anyio) which conflicts with
  pytest-asyncio auto mode and causes deadlocks on CI runners
- Remove unused --cov-report=html (only XML needed for Codecov)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/tests.yml:
- Line 12: The job-level timeout (timeout-minutes: 10) is killing whole workflow
runs on cold caches; either increase the job timeout to a safer value (e.g.,
timeout-minutes: 15) or remove the job-level timeout and instead add a
step-level timeout only around the test step (reference the timeout-minutes key
and the test step name used in the workflow) so setup/install/lint/typecheck can
complete while still bounding the test execution time; update the
.github/workflows/tests.yml accordingly.

TAOGenna and others added 4 commits February 18, 2026 11:20
- Disable langsmith pytest plugin that may phone home and block
- Set LANGSMITH_TRACING=false to prevent any tracing
- Add -v for verbose output to see exactly which test hangs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The test_main_module test spawns a subprocess that imports temporalio,
which loads gRPC. On Linux CI runners, gRPC starts non-daemon threads
that prevent the subprocess from exiting, causing subprocess.run to
block forever. Add timeout=10 to prevent the hang.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CLI tests calling dev() and serve() with config_file=None loaded
.olive.yaml from the repo root, which has all-default temporal config
(temporal.enabled=False). This caused tests to skip the temporal check
block and reach an unmocked uvicorn.run(), starting a real server that
blocks forever on Linux CI runners.

Fix: mock OliveConfig.from_file to return a config with temporal
explicitly enabled, so the tests exercise the intended code paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@TAOGenna TAOGenna self-requested a review February 18, 2026 16:50
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
tests/test_cli.py (2)

86-91: Move TemporalConfig import to the module-level imports.

TemporalConfig is imported lazily inside _temporal_enabled_config while OliveConfig is already imported at the module level (line 22). There's no circular-import or conditional-availability reason to defer it here.

♻️ Proposed refactor
-from olive.config import OliveConfig
+from olive.config import OliveConfig, TemporalConfig
 def _temporal_enabled_config(**overrides):
     """Return an OliveConfig with temporal explicitly enabled."""
-    from olive.config import TemporalConfig
-
     temporal_kwargs = {"enabled": True, **overrides}
     return OliveConfig(temporal=TemporalConfig(**temporal_kwargs))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_cli.py` around lines 86 - 91, Move the TemporalConfig import out
of the function and into the module-level imports alongside OliveConfig: remove
the lazy import inside _temporal_enabled_config and add a top-level "from ...
import TemporalConfig" so _temporal_enabled_config simply constructs
TemporalConfig(**temporal_kwargs) and returns OliveConfig(temporal=...), keeping
the function signature and behavior unchanged.

290-322: test_main_py_detection is the only dev()-exercising test that does not mock OliveConfig.from_file.

All other updated dev tests now mock OliveConfig.from_file for isolation. This test calls dev(config_file=None), which triggers a real .olive.yaml filesystem probe in the process's working directory. If the project root has such a file (or if CI is run from a directory that does), the loaded config could differ from the expected default (e.g., temporal.enabled=True), making the test environment-sensitive.

♻️ Proposed fix — add the same mock for consistency
 def test_main_py_detection():
     """Test that CLI detects main.py vs module import."""
     with (
         mock.patch("pathlib.Path.exists") as mock_exists,
+        mock.patch("olive.cli.OliveConfig.from_file", return_value=_temporal_enabled_config()),
         mock.patch("olive.cli.check_temporal_running", return_value=True),
         mock.patch("olive.cli.TemporalWorker"),
         mock.patch("uvicorn.run") as mock_uvicorn,
     ):
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_cli.py` around lines 290 - 322, test_main_py_detection calls
dev(config_file=None) and leaks a real .olive.yaml probe because
OliveConfig.from_file is not mocked; wrap the test's with(...) context to also
mock OliveConfig.from_file (the same symbol used in other dev tests) and have it
return a deterministic default OliveConfig (e.g., an instance representing
default settings with temporal.disabled or the same default used by other tests)
so the dev(host=..., config_file=None) invocation is isolated and deterministic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/test_cli.py`:
- Around line 86-91: Move the TemporalConfig import out of the function and into
the module-level imports alongside OliveConfig: remove the lazy import inside
_temporal_enabled_config and add a top-level "from ... import TemporalConfig" so
_temporal_enabled_config simply constructs TemporalConfig(**temporal_kwargs) and
returns OliveConfig(temporal=...), keeping the function signature and behavior
unchanged.
- Around line 290-322: test_main_py_detection calls dev(config_file=None) and
leaks a real .olive.yaml probe because OliveConfig.from_file is not mocked; wrap
the test's with(...) context to also mock OliveConfig.from_file (the same symbol
used in other dev tests) and have it return a deterministic default OliveConfig
(e.g., an instance representing default settings with temporal.disabled or the
same default used by other tests) so the dev(host=..., config_file=None)
invocation is isolated and deterministic.

The Temporal dev server database was accidentally committed. Added it to
.gitignore and moved the TemporalConfig import to the top of test_cli.py.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/test_cli.py (1)

288-320: ⚠️ Potential issue | 🟡 Minor

test_main_py_detection is fragile — global pathlib.Path.exists patch allows actual file I/O in OliveConfig.from_file.

mock.patch("pathlib.Path.exists") mocks the existence check but not the subsequent file read. When from_file(None) is called, it checks Path.cwd() / ".olive.yaml" with the mocked path.exists(), and if that returns True, it proceeds to execute the unmocked with open(path) call. Since .olive.yaml actually exists in the repo root, the test reads and parses the real configuration file, making test behavior dependent on that file's contents.

All other dev tests in this PR patch OliveConfig.from_file directly (e.g., line 95, 119, 146), preventing this issue.

Add mock.patch("olive.cli.OliveConfig.from_file", return_value=_temporal_enabled_config()) alongside the existing mocks to match the pattern used elsewhere and ensure the test is independent of the filesystem.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_cli.py` around lines 288 - 320, The test test_main_py_detection
currently patches pathlib.Path.exists but allows OliveConfig.from_file to
perform real file I/O; update the test's mock context to also patch
olive.cli.OliveConfig.from_file returning _temporal_enabled_config() (i.e., add
mock.patch("olive.cli.OliveConfig.from_file",
return_value=_temporal_enabled_config()) to the with(...) block) so dev(...)
uses the mocked config and the test no longer depends on the real .olive.yaml
file.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@tests/test_cli.py`:
- Around line 288-320: The test test_main_py_detection currently patches
pathlib.Path.exists but allows OliveConfig.from_file to perform real file I/O;
update the test's mock context to also patch olive.cli.OliveConfig.from_file
returning _temporal_enabled_config() (i.e., add
mock.patch("olive.cli.OliveConfig.from_file",
return_value=_temporal_enabled_config()) to the with(...) block) so dev(...)
uses the mocked config and the test no longer depends on the real .olive.yaml
file.

@TAOGenna TAOGenna removed the request for review from TerryCM February 18, 2026 19:06
@TAOGenna TAOGenna merged commit 31dc3c1 into main Feb 18, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants