From d4f4697533c02f318d6d0d98f1813a6dae4d9456 Mon Sep 17 00:00:00 2001 From: Utku Tugrul <10134382+utkutugrul@users.noreply.github.com> Date: Sat, 25 Jul 2026 05:45:31 -0700 Subject: [PATCH] runtime: resolve staged local-dir path to avoid symlink rejection on macOS (#857) --- strix/runtime/local_dir_staging.py | 2 +- tests/test_local_dir_staging.py | 35 ++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/strix/runtime/local_dir_staging.py b/strix/runtime/local_dir_staging.py index 4758aa5c..600d1125 100644 --- a/strix/runtime/local_dir_staging.py +++ b/strix/runtime/local_dir_staging.py @@ -110,7 +110,7 @@ def stage_symlink_safe_dir(src_root: Path) -> tuple[Path, Path | None]: if not tree_has_symlink(root): return root, None - staged = Path(tempfile.mkdtemp(prefix=_STAGING_PREFIX)) + staged = Path(tempfile.mkdtemp(prefix=_STAGING_PREFIX)).resolve() try: _stage_dir(root, staged, root, frozenset({root})) except OSError: diff --git a/tests/test_local_dir_staging.py b/tests/test_local_dir_staging.py index 89a60221..25e29391 100644 --- a/tests/test_local_dir_staging.py +++ b/tests/test_local_dir_staging.py @@ -106,3 +106,38 @@ def test_nested_symlinks_inside_linked_dir(tmp_path: Path) -> None: assert (staged / "pkg" / "shared_link" / "conf.json").read_text() == "{}\n" assert not (staged / "pkg" / "shared_link" / "escape").exists() assert not (staged / "shared" / "escape").exists() + + +def test_staged_path_has_no_symlink_ancestor(tmp_path: Path, monkeypatch) -> None: # noqa: ANN001 + """The staging directory itself must never sit behind a symlink. + + ``tempfile.mkdtemp()`` honors ``$TMPDIR``, and on macOS the default + ``$TMPDIR`` resolves through ``/var``, which is itself a symlink to + ``/private/var``. ``LocalDir`` rejects any symlink component in its + source path, so returning the raw ``mkdtemp()`` result breaks every + local-dir upload on macOS whenever the source tree contains a symlink. + This reproduces that shape without depending on the host OS layout. + """ + repo = _make_repo(tmp_path) + (repo / "link.py").symlink_to(repo / "pkg" / "mod.py") + + real_tmp_root = tmp_path / "real_tmp" + real_tmp_root.mkdir() + symlinked_tmp_root = tmp_path / "tmp_symlink" + symlinked_tmp_root.symlink_to(real_tmp_root) + + def fake_mkdtemp(prefix: str = "") -> str: + real_dir = real_tmp_root / f"{prefix}fake" + real_dir.mkdir() + return str(symlinked_tmp_root / real_dir.name) + + monkeypatch.setattr( + "strix.runtime.local_dir_staging.tempfile.mkdtemp", fake_mkdtemp + ) + + upload_path, staged = stage_symlink_safe_dir(repo) + + assert staged is not None + assert upload_path == staged + for path in (staged, *staged.parents): + assert not path.is_symlink(), f"staged path has a symlink ancestor: {path}"