@@ -296,6 +296,8 @@ async def create_greeting_activity(info: GreetingInfo) -> str:
296
296
297
297
Some things to note about the above code:
298
298
299
+ * Workflows run in a sandbox by default. Users are encouraged to define workflows in files with no side effects or other
300
+ complicated code. See the [ Workflow Sandbox] ( #workflow-sandbox ) section for more details.
299
301
* This workflow continually updates the queryable current greeting when signalled and can complete with the greeting on
300
302
a different signal
301
303
* Workflows are always classes and must have a single ` @workflow.run ` which is an ` async def ` function
@@ -581,6 +583,145 @@ method.
581
583
Activities are just functions decorated with ` @activity.defn ` . Simply write different ones and pass those to the worker
582
584
to have different activities called during the test.
583
585
586
+ #### Workflow Sandbox
587
+
588
+ By default workflows are run in a sandbox to help avoid non-deterministic code. If a call that is known to be
589
+ non-deterministic is performed, an exception will be thrown in the workflow which will "fail the task" which means the
590
+ workflow will not progress until fixed.
591
+
592
+ The sandbox is not foolproof and non-determinism can still occur. It is simply a best-effort way to catch bad code
593
+ early. Users are encouraged to define their workflows in files with no other side effects.
594
+
595
+ ##### How the Sandbox Works
596
+
597
+ The sandbox is made up of two components that work closely together:
598
+
599
+ * Global state isolation
600
+ * Restrictions preventing known non-deterministic library calls
601
+
602
+ Global state isolation is performed by using ` exec ` . Upon workflow start, the file that the workflow is defined in is
603
+ imported into a new sandbox created for that workflow run. In order to keep the sandbox performant a known set of
604
+ "passthrough modules" are passed through from outside of the sandbox when they are imported. These are expected to be
605
+ side-effect free on import and have their non-deterministic aspects restricted. By default the entire Python standard
606
+ library, ` temporalio ` , and a couple of other modules are passed through from outside of the sandbox. To update this
607
+ list, see "Customizing the Sandbox".
608
+
609
+ Restrictions preventing known non-deterministic library calls are achieved using proxy objects on modules wrapped around
610
+ the custom importer set in the sandbox. Many restrictions apply at workflow import time and workflow run time, while
611
+ some restrictions only apply at workflow run time. A default set of restrictions is included that prevents most
612
+ dangerous standard library calls. However it is known in Python that some otherwise-non-deterministic invocations, like
613
+ reading a file from disk via ` open ` or using ` os.environ ` , are done as part of importing modules. To customize what is
614
+ and isn't restricted, see "Customizing the Sandbox".
615
+
616
+ ##### Avoiding the Sandbox
617
+
618
+ There are three increasingly-scoped ways to avoid the sandbox. Users are discouraged from avoiding the sandbox if
619
+ possible.
620
+
621
+ To remove restrictions around a particular block of code, use ` with temporalio.workflow.unsafe.sandbox_unrestricted(): ` .
622
+ The workflow will still be running in the sandbox, but no restrictions for invalid library calls will be applied.
623
+
624
+ To run an entire workflow outside of a sandbox, set ` sandboxed=False ` on the ` @workflow.defn ` decorator when defining
625
+ it. This will run the entire workflow outside of the workflow which means it can share global state and other bad
626
+ things.
627
+
628
+ To disable the sandbox entirely for a worker, set the ` Worker ` init's ` workflow_runner ` keyword argument to
629
+ ` temporalio.worker.UnsandboxedWorkflowRunner() ` . This value is defaulted to
630
+ ` temporalio.worker.workflow_sandbox.SandboxedWorkflowRunner() ` so by changing it to the unsandboxed runner, the sandbox
631
+ will not be used at all.
632
+
633
+ ##### Customizing the Sandbox
634
+
635
+ ⚠️ WARNING: APIs in the ` temporalio.worker.workflow_sandbox ` module are not yet considered stable and may change in
636
+ future releases.
637
+
638
+ When creating the ` Worker ` , the ` workflow_runner ` is defaulted to
639
+ ` temporalio.worker.workflow_sandbox.SandboxedWorkflowRunner() ` . The ` SandboxedWorkflowRunner ` 's init accepts a
640
+ ` restrictions ` keyword argument that is defaulted to ` SandboxRestrictions.default ` . The ` SandboxRestrictions ` dataclass
641
+ is immutable and contains three fields that can be customized, but only two have notable value
642
+
643
+ ###### Passthrough Modules
644
+
645
+ To make the sandbox quicker when importing known third party libraries, they can be added to the
646
+ ` SandboxRestrictions.passthrough_modules ` set like so:
647
+
648
+ ``` python
649
+ my_restrictions = dataclasses.replace(
650
+ SandboxRestrictions.default,
651
+ passthrough_modules = SandboxRestrictions.passthrough_modules_default | SandboxMatcher(access = {" pydantic" }),
652
+ )
653
+ my_worker = Worker(... , runner = SandboxedWorkflowRunner(restrictions = my_restrictions))
654
+ ```
655
+
656
+ If an "access" match succeeds for an import, it will simply be forwarded from outside of the sandbox. See the API for
657
+ more details on exact fields and their meaning.
658
+
659
+ ###### Invalid Module Members
660
+
661
+ ` SandboxRestrictions.invalid_module_members ` contains a root matcher that applies to all module members. This already
662
+ has a default set which includes things like ` datetime.date.today() ` which should never be called from a workflow. To
663
+ remove this restriction:
664
+
665
+ ``` python
666
+ my_restrictions = dataclasses.replace(
667
+ SandboxRestrictions.default,
668
+ invalid_module_members = SandboxRestrictions.invalid_module_members_default.with_child_unrestricted(
669
+ " datetime" , " date" , " today" ,
670
+ ),
671
+ )
672
+ my_worker = Worker(... , runner = SandboxedWorkflowRunner(restrictions = my_restrictions))
673
+ ```
674
+
675
+ Restrictions can also be added by ` | ` 'ing together matchers, for example to restrict the ` datetime.date ` class from
676
+ being used altogether:
677
+
678
+ ``` python
679
+ my_restrictions = dataclasses.replace(
680
+ SandboxRestrictions.default,
681
+ invalid_module_members = SandboxRestrictions.invalid_module_members_default | SandboxMatcher(
682
+ children = {" datetime" : SandboxMatcher(use = {" date" })},
683
+ ),
684
+ )
685
+ my_worker = Worker(... , runner = SandboxedWorkflowRunner(restrictions = my_restrictions))
686
+ ```
687
+
688
+ See the API for more details on exact fields and their meaning.
689
+
690
+ ##### Known Sandbox Issues
691
+
692
+ Below are known sandbox issues. As the sandbox is developed and matures, some may be resolved.
693
+
694
+ ###### Global Import/Builtins
695
+
696
+ Currently the sandbox references/alters the global ` sys.modules ` and ` builtins ` fields while running workflow code. In
697
+ order to prevent affecting other sandboxed code, thread locals are leveraged to only intercept these values during the
698
+ workflow thread running. Therefore, technically if top-level import code starts a thread, it may lose sandbox
699
+ protection.
700
+
701
+ ###### Sandbox is not Secure
702
+
703
+ The sandbox is built to catch many non-deterministic and state sharing issues, but it is not secure. Some known bad
704
+ calls are intercepted, but for performance reasons, every single attribute get/set cannot be checked. Therefore a simple
705
+ call like ` setattr(temporalio.common, "__my_key", "my value") ` will leak across sandbox runs.
706
+
707
+ The sandbox is only a helper, it does not provide full protection.
708
+
709
+ ###### Sandbox Performance
710
+
711
+ TODO: This is actively being measured; results to come soon
712
+
713
+ ###### Extending Restricted Classes
714
+
715
+ Currently, extending classes marked as restricted causes an issue with their ` __init__ ` parameters. This does not affect
716
+ most users, but if there is a dependency that is, say, extending ` zipfile.ZipFile ` an error may occur and the module
717
+ will have to be marked as pass through.
718
+
719
+ ###### is_subclass of ABC-based Restricted Classes
720
+
721
+ Due to [ https://bugs.python.org/issue44847 ] ( https://bugs.python.org/issue44847 ) , classes that are wrapped and then
722
+ checked to see if they are subclasses of another via ` is_subclass ` may fail (see also
723
+ [ this wrapt issue] ( https://github.com/GrahamDumpleton/wrapt/issues/130 ) ).
724
+
584
725
### Activities
585
726
586
727
#### Definition
0 commit comments