From e0495ebc38d1303ec906bab1bad34140ff56ddbc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 6 May 2026 21:27:40 +0000 Subject: [PATCH] Fix arc_tmp_path UnboundLocalError in workflow install; add preset symlink rejection test Agent-Logs-Url: https://github.com/github/spec-kit/sessions/0469bac5-296a-46b6-b84e-eb33b0dc0fce Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com> --- src/specify_cli/__init__.py | 17 +++++++++++------ tests/test_presets.py | 14 ++++++++++++++ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index 8d1e97215..1da35136c 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -5079,16 +5079,18 @@ def workflow_add( if archive_fmt in ("tar.gz", "zip"): # Extract workflow.yml from the archive. suffix = ".tar.gz" if archive_fmt == "tar.gz" else ".zip" + arc_tmp_path = None with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as arc_tmp: - arc_tmp.write(raw_data) arc_tmp_path = Path(arc_tmp.name) + arc_tmp.write(raw_data) try: wf_yaml = _extract_workflow_yml(arc_tmp_path, archive_fmt) with tempfile.NamedTemporaryFile(suffix=".yml", delete=False) as tmp: - tmp.write(wf_yaml) tmp_path = Path(tmp.name) + tmp.write(wf_yaml) finally: - arc_tmp_path.unlink(missing_ok=True) + if arc_tmp_path is not None: + arc_tmp_path.unlink(missing_ok=True) else: # Treat as a plain YAML file (existing behaviour). with tempfile.NamedTemporaryFile(suffix=".yml", delete=False) as tmp: @@ -5125,9 +5127,10 @@ def workflow_add( console.print(f"[red]Error:[/red] Failed to extract workflow from archive: {exc}") raise typer.Exit(1) import tempfile + tmp_local = None with tempfile.NamedTemporaryFile(suffix=".yml", delete=False) as tmp: - tmp.write(wf_yaml) tmp_local = Path(tmp.name) + tmp.write(wf_yaml) try: _validate_and_install_local(tmp_local, str(source_path)) finally: @@ -5231,13 +5234,15 @@ def workflow_add( if cat_archive_fmt in ("tar.gz", "zip"): # Download URL points to an archive — extract workflow.yml from it. suffix = ".tar.gz" if cat_archive_fmt == "tar.gz" else ".zip" + arc_tmp = None with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as arc_f: - arc_f.write(raw_response) arc_tmp = Path(arc_f.name) + arc_f.write(raw_response) try: wf_yaml_bytes = _extract_workflow_yml(arc_tmp, cat_archive_fmt) finally: - arc_tmp.unlink(missing_ok=True) + if arc_tmp is not None: + arc_tmp.unlink(missing_ok=True) workflow_file.write_bytes(wf_yaml_bytes) else: workflow_file.write_bytes(raw_response) diff --git a/tests/test_presets.py b/tests/test_presets.py index f6f0a5a08..133600d99 100644 --- a/tests/test_presets.py +++ b/tests/test_presets.py @@ -719,6 +719,20 @@ class TestPresetManager: with pytest.raises(PresetValidationError, match="Unsafe path"): manager.install_from_zip(archive, "0.1.5") + def test_install_from_tar_gz_rejects_symlinks(self, project_dir, temp_dir): + """install_from_zip must reject tarballs containing symlinks.""" + import tarfile + archive = temp_dir / "symlink.tar.gz" + with tarfile.open(archive, "w:gz") as tf: + info = tarfile.TarInfo(name="link") + info.type = tarfile.SYMTYPE + info.linkname = "/etc/passwd" + tf.addfile(info) + + manager = PresetManager(project_dir) + with pytest.raises(PresetValidationError, match="Symlinks"): + manager.install_from_zip(archive, "0.1.5") + def test_remove(self, project_dir, pack_dir): """Test removing a preset.""" manager = PresetManager(project_dir)