@@ -109,6 +109,65 @@ def test_link_mode_symlinks_files_but_never_mutates_source_task_toml(tmp_path):
109109 assert "RAZORBACK_BENCHMARK_TASK_ID" not in (source / "task.toml" ).read_text ()
110110
111111
112+ def test_link_mode_copies_environment_build_context_as_real_files (tmp_path ):
113+ """`view_mode="link"` must keep the `environment/` Docker build context as
114+ REAL files, not symlinks. `docker compose build` runs with the view's
115+ `environment/` dir as the build context; BuildKit cannot read a Dockerfile
116+ that symlinks outside the context (`failed to read dockerfile: no such file
117+ or directory`), so a symlinked build context breaks every build-from-source
118+ benchmark (e.g. swe-bench-pro) under the default bind/link mode. Bulk task
119+ files still symlink — only the build context is forced real.
120+ """
121+ source = _write_source_task (tmp_path )
122+ dockerfile_text = (source / "environment" / "Dockerfile" ).read_text ()
123+
124+ view = materialize_harbor_task_view (
125+ source_task_dir = source ,
126+ view_root = tmp_path / "views" ,
127+ benchmark_kind = "fixture-bench" ,
128+ benchmark_task_id = "task-001" ,
129+ transform_name = "fixture-transform" ,
130+ view_mode = "link" ,
131+ )
132+
133+ # the Docker build context is a real, view-owned file (readable by BuildKit)
134+ assert (view / "environment" / "Dockerfile" ).is_file ()
135+ assert not (view / "environment" / "Dockerfile" ).is_symlink ()
136+ assert (view / "environment" / "Dockerfile" ).read_text () == dockerfile_text
137+ # bulk files outside environment/ still symlink — the bind/link contract holds
138+ assert (view / "instruction.md" ).is_symlink ()
139+ assert (view / "data" / "input.csv" ).is_symlink ()
140+
141+
142+ def test_environment_symlink_to_denied_target_is_not_smuggled (tmp_path ):
143+ """A symlink under `environment/` with an innocuous name must not smuggle a
144+ DENIED target's bytes into the view. Copying the build context follows
145+ symlinks (shutil.copy2), and the name-based deny filter only sees the link's
146+ own path — so `environment/leak.patch -> ../gold_patch.diff` would otherwise
147+ embed gold-patch content under an allowed view path. The materializer must
148+ resolve the target and re-apply the deny check, dropping it.
149+ """
150+ source = _write_source_task (tmp_path )
151+ (source / "gold_patch.diff" ).write_text ("--- GOLD ANSWER PATCH ---\n " )
152+ (source / "environment" / "leak.patch" ).symlink_to (source / "gold_patch.diff" )
153+
154+ view = materialize_harbor_task_view (
155+ source_task_dir = source ,
156+ view_root = tmp_path / "views" ,
157+ benchmark_kind = "fixture-bench" ,
158+ benchmark_task_id = "task-001" ,
159+ transform_name = "fixture-transform" ,
160+ exclude_globs = ("gold_patch*" , "gold.patch" ),
161+ view_mode = "link" ,
162+ )
163+
164+ # the legit build context is still materialized as a real file …
165+ assert (view / "environment" / "Dockerfile" ).is_file ()
166+ assert not (view / "environment" / "Dockerfile" ).is_symlink ()
167+ # … but the disguised symlink to a denied target is dropped, not embedded
168+ assert not (view / "environment" / "leak.patch" ).exists ()
169+
170+
112171def test_materialized_view_is_harbor_taskconfig_path_ready (tmp_path ):
113172 source = _write_source_task (tmp_path )
114173
0 commit comments