From 9d1030e72ae20b5c51dca40e966641b919ccb951 Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Mon, 5 Jan 2026 10:47:30 -0500 Subject: [PATCH 1/4] Add a way to disable sandboxing on workflow logger --- temporalio/workflow.py | 24 ++++++++++++++++- tests/worker/test_workflow.py | 49 +++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/temporalio/workflow.py b/temporalio/workflow.py index 90daecbe2..f99310289 100644 --- a/temporalio/workflow.py +++ b/temporalio/workflow.py @@ -1565,6 +1565,7 @@ def __init__(self, logger: logging.Logger, extra: Mapping[str, Any] | None) -> N self.workflow_info_on_extra = True self.full_workflow_info_on_extra = False self.log_during_replay = False + self._unsafe_disable_sandbox = False def process( self, msg: Any, kwargs: MutableMapping[str, Any] @@ -1598,7 +1599,22 @@ def process( kwargs["extra"] = {**extra, **(kwargs.get("extra") or {})} if msg_extra: msg = f"{msg} ({msg_extra})" - return (msg, kwargs) + return msg, kwargs + + def log( + self, + level: int, + msg: object, + *args: Any, + **kwargs: Any, + ): + """Override to potentially disable the sandbox.""" + if self._unsafe_disable_sandbox: + with unsafe.sandbox_unrestricted(): + with unsafe.imports_passed_through(): + super().log(level, msg, *args, **kwargs) + else: + super().log(level, msg, *args, **kwargs) def isEnabledFor(self, level: int) -> bool: """Override to ignore replay logs.""" @@ -1613,6 +1629,12 @@ def base_logger(self) -> logging.Logger: """ return self.logger + def unsafe_disable_sandbox(self, value: bool = True): + """Disable the sandbox during log processing. + Can be turned back on with unsafe_disable_sandbox(False). + """ + self._unsafe_disable_sandbox = value + logger = LoggerAdapter(logging.getLogger(__name__), None) """Logger that will have contextual workflow details embedded. diff --git a/tests/worker/test_workflow.py b/tests/worker/test_workflow.py index 8c7feae82..f21090e01 100644 --- a/tests/worker/test_workflow.py +++ b/tests/worker/test_workflow.py @@ -8432,3 +8432,52 @@ async def test_activity_failure_with_encoded_payload_is_decoded_in_workflow( run_timeout=timedelta(seconds=5), ) assert result == "Handled encrypted failure successfully" + + +@workflow.defn +class DisableLoggerSandbox: + @workflow.run + async def run(self): + workflow.logger.info("Running workflow") + + +class CustomLogHandler(logging.Handler): + def emit(self, record: logging.LogRecord) -> None: + import httpx # type: ignore[reportUnusedImport] + + +async def test_disable_logger_sandbox( + client: Client, +): + logger = workflow.logger.logger + logger.addHandler(CustomLogHandler()) + async with new_worker( + client, + DisableLoggerSandbox, + activities=[], + ) as worker: + with pytest.raises(WorkflowFailureError): + await client.execute_workflow( + DisableLoggerSandbox.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + run_timeout=timedelta(seconds=1), + retry_policy=RetryPolicy(maximum_attempts=1), + ) + workflow.logger.unsafe_disable_sandbox() + await client.execute_workflow( + DisableLoggerSandbox.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + run_timeout=timedelta(seconds=1), + retry_policy=RetryPolicy(maximum_attempts=1), + ) + workflow.logger.unsafe_disable_sandbox(False) + with pytest.raises(WorkflowFailureError): + await client.execute_workflow( + DisableLoggerSandbox.run, + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + run_timeout=timedelta(seconds=1), + retry_policy=RetryPolicy(maximum_attempts=1), + ) From 506908d187fea5eb77ce081c288a86f25fd5cb16 Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Tue, 6 Jan 2026 13:37:13 -0500 Subject: [PATCH 2/4] Increment stack level to skip override in stack --- temporalio/workflow.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/temporalio/workflow.py b/temporalio/workflow.py index f99310289..857da7b28 100644 --- a/temporalio/workflow.py +++ b/temporalio/workflow.py @@ -1565,7 +1565,7 @@ def __init__(self, logger: logging.Logger, extra: Mapping[str, Any] | None) -> N self.workflow_info_on_extra = True self.full_workflow_info_on_extra = False self.log_during_replay = False - self._unsafe_disable_sandbox = False + self.disable_sandbox = False def process( self, msg: Any, kwargs: MutableMapping[str, Any] @@ -1606,15 +1606,17 @@ def log( level: int, msg: object, *args: Any, + stacklevel: int = 1, **kwargs: Any, ): """Override to potentially disable the sandbox.""" - if self._unsafe_disable_sandbox: + stacklevel += 1 + if self.disable_sandbox: with unsafe.sandbox_unrestricted(): with unsafe.imports_passed_through(): - super().log(level, msg, *args, **kwargs) + super().log(level, msg, *args, stacklevel=stacklevel, **kwargs) else: - super().log(level, msg, *args, **kwargs) + super().log(level, msg, *args, stacklevel=stacklevel, **kwargs) def isEnabledFor(self, level: int) -> bool: """Override to ignore replay logs.""" @@ -1633,7 +1635,7 @@ def unsafe_disable_sandbox(self, value: bool = True): """Disable the sandbox during log processing. Can be turned back on with unsafe_disable_sandbox(False). """ - self._unsafe_disable_sandbox = value + self.disable_sandbox = value logger = LoggerAdapter(logging.getLogger(__name__), None) From 21e0f2aff36a029415677e7048b9bc551f1f6416 Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Wed, 7 Jan 2026 13:44:01 -0500 Subject: [PATCH 3/4] Extra stacklevel is needed on 3.10 --- temporalio/workflow.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/temporalio/workflow.py b/temporalio/workflow.py index 857da7b28..cd5f28d6b 100644 --- a/temporalio/workflow.py +++ b/temporalio/workflow.py @@ -6,6 +6,7 @@ import contextvars import inspect import logging +import sys import threading import typing import uuid @@ -1610,6 +1611,9 @@ def log( **kwargs: Any, ): """Override to potentially disable the sandbox.""" + if sys.version_info < (3, 11): + # An additional stacklevel is needed on 3.10 because it doesn't skip internal frames until after stacklevel + stacklevel += 1 stacklevel += 1 if self.disable_sandbox: with unsafe.sandbox_unrestricted(): From 2e7c84ad27e369a1fd7bee62441206a43d790c8a Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Wed, 7 Jan 2026 14:43:32 -0500 Subject: [PATCH 4/4] Lint suppression --- temporalio/workflow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/temporalio/workflow.py b/temporalio/workflow.py index cd5f28d6b..a6f0805e3 100644 --- a/temporalio/workflow.py +++ b/temporalio/workflow.py @@ -1613,7 +1613,7 @@ def log( """Override to potentially disable the sandbox.""" if sys.version_info < (3, 11): # An additional stacklevel is needed on 3.10 because it doesn't skip internal frames until after stacklevel - stacklevel += 1 + stacklevel += 1 # type: ignore[reportUnreachable] stacklevel += 1 if self.disable_sandbox: with unsafe.sandbox_unrestricted():