From 81322a1df6923327a9df44151ca695928f4e642a Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Tue, 23 Jun 2026 13:44:09 -0700 Subject: [PATCH 1/3] workflows: add some os things to sandbox whitelist, only restrict callables shutil, pathlib and third-party rich were both failing to import due to sandbox interactions with os. I'm adding getenv, getcwd, and PathLike to the whitelist. I've also made it so that we only restrict callables, since we were restricting the set `os.supports_dir_fd` which was breaking some stuff. (We could also have added it to the whitelist but this seems a little more general, though we will want to further generalize it later.) --- src/vercel/_internal/workflow/py_sandbox.py | 9 +++++++++ tests/unit/test_py_sandbox.py | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/src/vercel/_internal/workflow/py_sandbox.py b/src/vercel/_internal/workflow/py_sandbox.py index 829568cc..926fe991 100644 --- a/src/vercel/_internal/workflow/py_sandbox.py +++ b/src/vercel/_internal/workflow/py_sandbox.py @@ -26,6 +26,8 @@ class SandboxRestrictionError(RuntimeError): """Raised when workflow code calls a non-deterministic function.""" +# TODO: We should have a more proper proxy that blocks __call__ and +# returns proxied members but otherwise looks the same. def _restricted(name: str) -> Callable[..., NoReturn]: def _raise(*_args: Any, **_kwargs: Any) -> NoReturn: raise SandboxRestrictionError( @@ -288,7 +290,12 @@ def _current_task(loop: Any = None) -> Any: "fsdecode", "fsencode", "fspath", + # These are deterministic enough if the functions that change + # them are blocked... + "getenv", + "getcwd", "_get_exports_list", + "PathLike", environ=os.environ.copy(), allow_if=str.isupper, drops=["fork", "register_at_fork"], @@ -488,6 +495,8 @@ def __getattr__(self, name: str) -> Any: and name not in policy.allowed and not (name.startswith("__") and name.endswith("__")) and not (policy.allow_if is not None and policy.allow_if(name)) + # Only restrict callables (which will include classes). + and callable(policy.resolve_attr(name, real)) ): # Return a restricted callable instead of raising immediately. # This allows module init code like ``from os import urandom`` diff --git a/tests/unit/test_py_sandbox.py b/tests/unit/test_py_sandbox.py index 0fa88eaf..3ff5a795 100644 --- a/tests/unit/test_py_sandbox.py +++ b/tests/unit/test_py_sandbox.py @@ -874,6 +874,14 @@ def test_math_works(self): ns = _run_in_sandbox("import math; result = math.sqrt(16)") assert ns["result"] == 4.0 + def test_shutil_works(self): + ns = _run_in_sandbox("import os; lurr = os.supports_dir_fd") + _run_in_sandbox("import shutil") + + def test_pathlib_works(self): + ns = _run_in_sandbox("import pathlib; result = isinstance(0, pathlib.Path)") + assert not ns['result'] + def test_collections_counter(self): ns = _run_in_sandbox("from collections import Counter; result = dict(Counter('aabbc'))") assert ns["result"] == {"a": 2, "b": 2, "c": 1} From 86d19198a1e67a467f433ed32cd34c6d9dcb2e76 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Tue, 23 Jun 2026 16:27:19 -0700 Subject: [PATCH 2/3] tests: fix lint errors in test_py_sandbox --- tests/unit/test_py_sandbox.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_py_sandbox.py b/tests/unit/test_py_sandbox.py index 3ff5a795..cfad76f8 100644 --- a/tests/unit/test_py_sandbox.py +++ b/tests/unit/test_py_sandbox.py @@ -875,12 +875,12 @@ def test_math_works(self): assert ns["result"] == 4.0 def test_shutil_works(self): - ns = _run_in_sandbox("import os; lurr = os.supports_dir_fd") + _run_in_sandbox("import os; lurr = os.supports_dir_fd") _run_in_sandbox("import shutil") def test_pathlib_works(self): ns = _run_in_sandbox("import pathlib; result = isinstance(0, pathlib.Path)") - assert not ns['result'] + assert not ns["result"] def test_collections_counter(self): ns = _run_in_sandbox("from collections import Counter; result = dict(Counter('aabbc'))") From ec82993ea372aa58a034cf235783c0676219bcc2 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Tue, 23 Jun 2026 16:39:46 -0700 Subject: [PATCH 3/3] tests: getcwd is now allowed in sandbox The sandbox allowlist now permits os.getcwd(); update the test to match the new behavior instead of asserting it raises. --- tests/unit/test_py_sandbox.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_py_sandbox.py b/tests/unit/test_py_sandbox.py index cfad76f8..a79d63ad 100644 --- a/tests/unit/test_py_sandbox.py +++ b/tests/unit/test_py_sandbox.py @@ -136,8 +136,9 @@ def test_os_fspath_allowed(self): def test_os_constants_allowed(self): _run_in_sandbox("import os; _ = os.O_RDONLY") - def test_os_getcwd_blocked(self): - _raises_in_sandbox("import os; os.getcwd()") + def test_os_getcwd_allowed(self): + ns = _run_in_sandbox("import os; result = os.getcwd()") + assert isinstance(ns["result"], str) def test_os_listdir_blocked(self): _raises_in_sandbox("import os; os.listdir('.')")