mirror of
https://github.com/priyanshujain/messageboardbench.git
synced 2026-10-02 11:07:07 +00:00
Support immutable local SWE image IDs
This commit is contained in:
1 parent
69c5793562
commit
1428c2b08e
9 files changed
+129
-43
No files matched your search
@@ -90,7 +90,8 @@ def environment_validation_matches_plan(
|
||||
if (
|
||||
set(row) != {
|
||||
"instance_id", "manifest_path", "manifest_sha256",
|
||||
"validated_image", "validated_image_id", "validated_repo_digest"
|
||||
"validated_image", "validated_image_id", "validated_image_ref",
|
||||
"validated_repo_digest",
|
||||
}
|
||||
or not Path(row["manifest_path"]).as_posix().endswith(
|
||||
"/" + Path(entry["path"]).as_posix()
|
||||
@@ -98,7 +99,11 @@ def environment_validation_matches_plan(
|
||||
or row["manifest_sha256"] != entry["sha256"]
|
||||
or row["validated_image"] != validated["image"]
|
||||
or row["validated_image_id"] != remote_image["id"]
|
||||
or row["validated_repo_digest"] != remote_image["repo_digests"][0]
|
||||
or row["validated_image_ref"] != remote_image["immutable_ref"]
|
||||
or row["validated_repo_digest"] != (
|
||||
remote_image["repo_digests"][0]
|
||||
if remote_image["repo_digests"] else None
|
||||
)
|
||||
):
|
||||
return False
|
||||
except (KeyError, OSError, ValueError, json.JSONDecodeError):
|
||||
|
||||
@@ -105,7 +105,6 @@ def recover_terminal_rows(out: Path) -> list[dict]:
|
||||
|
||||
def cleanup_matched_images(
|
||||
out: Path, team: int, cohort: int, instance_ids: list[str], records: dict,
|
||||
validated_images: dict[str, str] | None = None,
|
||||
) -> None:
|
||||
"""Remove only explicit, re-pullable tags after both matched arms terminate."""
|
||||
path = out / "image-lifecycle.json"
|
||||
@@ -117,7 +116,9 @@ def cleanup_matched_images(
|
||||
record = {"team": team, "cohort": cohort, "images": []}
|
||||
failed = False
|
||||
for instance_id in instance_ids:
|
||||
image = (validated_images or {}).get(instance_id) or swebench_spec(records[instance_id])[0]
|
||||
# Remove the mutable local tag, not an immutable ID/digest that may still
|
||||
# have another tag reference. Identity was already frozen before execution.
|
||||
image = swebench_spec(records[instance_id])[0]
|
||||
inspected = subprocess.run(
|
||||
["docker", "image", "inspect", image, "--format", "{{json .}}"],
|
||||
capture_output=True, text=True, env=os.environ,
|
||||
@@ -247,7 +248,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
team_plans = plan["team_plans"]
|
||||
configs = out / "compose"
|
||||
validated_images = {
|
||||
row["instance_id"]: row["validated_repo_digest"]
|
||||
row["instance_id"]: row["validated_image_ref"]
|
||||
for row in (environment_validation or {}).get("validated_instances", [])
|
||||
}
|
||||
compose_by_assignment = {
|
||||
@@ -366,7 +367,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
if all((team, arm, instance_id) in terminal
|
||||
for arm in CONDITIONS for instance_id in selected):
|
||||
cleanup_matched_images(
|
||||
out, team, cohort, selected, records, validated_images
|
||||
out, team, cohort, selected, records
|
||||
)
|
||||
continue
|
||||
tasks = []
|
||||
@@ -460,7 +461,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
)
|
||||
if matched_complete:
|
||||
cleanup_matched_images(
|
||||
out, team, cohort, selected, records, validated_images
|
||||
out, team, cohort, selected, records
|
||||
)
|
||||
status["status"] = "completed"
|
||||
except BaseException as exc:
|
||||
|
||||
@@ -26,8 +26,8 @@ from inspect_ai.util import SandboxEnvironmentSpec, sandbox
|
||||
from messageboardbench.board import MESSAGEBOARD_V2_INTERFACE_VERSION, board_tools
|
||||
from messageboardbench.feedback import feedback_tool
|
||||
from messageboardbench.swe_validation import (
|
||||
DATASET, GRADING_LIFECYCLE, normalize_record, patch_files, require_revision,
|
||||
run_fresh_grader, swebench_spec,
|
||||
DATASET, GRADING_LIFECYCLE, is_immutable_image_reference, normalize_record,
|
||||
patch_files, require_revision, run_fresh_grader, swebench_spec,
|
||||
)
|
||||
|
||||
|
||||
@@ -345,8 +345,8 @@ def write_compose(
|
||||
) -> Path:
|
||||
image, _, _ = swebench_spec(record)
|
||||
if image_override is not None:
|
||||
if "@sha256:" not in image_override:
|
||||
raise ValueError("validated image override must be a repository digest")
|
||||
if not is_immutable_image_reference(image_override):
|
||||
raise ValueError("validated image override must be an immutable image reference")
|
||||
image = image_override
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
path = directory / (str(record["instance_id"]).replace("/", "_") + ".yaml")
|
||||
@@ -367,8 +367,8 @@ def sample_from_record(
|
||||
# other branch. Make the intended upstream choice explicit.
|
||||
metadata["impossible_patch"] = ""
|
||||
if grader_image is not None:
|
||||
if "@sha256:" not in grader_image:
|
||||
raise ValueError("fresh grader image must be a repository digest")
|
||||
if not is_immutable_image_reference(grader_image):
|
||||
raise ValueError("fresh grader image must be an immutable image reference")
|
||||
metadata["messageboardbench_grader_image"] = grader_image
|
||||
return Sample(
|
||||
id=str(row["instance_id"]),
|
||||
@@ -568,8 +568,9 @@ def swe_board_scorer(*, memory: str = "8g", timeout_seconds: int = 600):
|
||||
"problem_statement": state.input,
|
||||
}
|
||||
grader_image = state.metadata.get("messageboardbench_grader_image")
|
||||
if not isinstance(grader_image, str) or "@sha256:" not in grader_image:
|
||||
raise RuntimeError("missing validated repository digest for fresh grader")
|
||||
if (not isinstance(grader_image, str)
|
||||
or not is_immutable_image_reference(grader_image)):
|
||||
raise RuntimeError("missing validated immutable image reference for fresh grader")
|
||||
evaluated, output, statuses, eval_script_sha256, _ = await asyncio.to_thread(
|
||||
run_fresh_grader,
|
||||
record,
|
||||
|
||||
@@ -65,7 +65,10 @@ def validate_environment_index_for_records(
|
||||
"manifest_sha256": entry["sha256"],
|
||||
"validated_image": manifest["image"],
|
||||
"validated_image_id": remote_image["id"],
|
||||
"validated_repo_digest": remote_image["repo_digests"][0],
|
||||
"validated_image_ref": remote_image["immutable_ref"],
|
||||
"validated_repo_digest": (
|
||||
remote_image["repo_digests"][0] if remote_image["repo_digests"] else None
|
||||
),
|
||||
})
|
||||
return {"index_path": str(index_path), "index_sha256": _sha(index_path),
|
||||
"validated_instances": evidence}
|
||||
@@ -187,9 +190,13 @@ def validate_task_manifest(
|
||||
if len(commands) != 1 or list(next(iter(commands))) != manifest.get("test_command"):
|
||||
raise ValueError(f"validation test command mismatch: {instance_id}")
|
||||
image_id, repo_digests = next(iter(identities))
|
||||
if manifest.get("remote_image") != {
|
||||
"id": image_id, "repo_digests": list(repo_digests)
|
||||
} or not repo_digests:
|
||||
from messageboardbench.swe_validation import immutable_image_reference
|
||||
expected_remote_image = {
|
||||
"id": image_id,
|
||||
"repo_digests": list(repo_digests),
|
||||
"immutable_ref": immutable_image_reference(image_id, repo_digests),
|
||||
}
|
||||
if manifest.get("remote_image") != expected_remote_image:
|
||||
raise ValueError(f"validation remote image mismatch: {instance_id}")
|
||||
for row in results:
|
||||
output = manifest_path.parent / str(row.get("output_file", ""))
|
||||
|
||||
@@ -19,6 +19,8 @@ DATASET = "fjzzq2002/impossible_swebench"
|
||||
REMOTE_DOCKER_HOST = "ssh://[email protected]"
|
||||
GRADING_LIFECYCLE = "upstream-testspec-eval-commands-status-monitored-v2"
|
||||
FULL_SHA = re.compile(r"[0-9a-f]{40}\Z")
|
||||
IMAGE_ID = re.compile(r"sha256:[0-9a-f]{64}\Z")
|
||||
REPO_DIGEST = re.compile(r"[^\s@]+@sha256:[0-9a-f]{64}\Z")
|
||||
PATCH_PATH_RE = re.compile(r"^(?:--- a/|\+\+\+ b/)(.+)$", re.MULTILINE)
|
||||
|
||||
|
||||
@@ -207,14 +209,28 @@ def image_identity(
|
||||
platform = f"{value['Os']}/{value['Architecture']}"
|
||||
except (KeyError, TypeError, json.JSONDecodeError) as exc:
|
||||
raise ValidationError("Docker returned an invalid image identity") from exc
|
||||
if platform != "linux/amd64" or not image_id or not repo_digests:
|
||||
if (platform != "linux/amd64" or not IMAGE_ID.fullmatch(str(image_id))
|
||||
or any(not REPO_DIGEST.fullmatch(str(value)) for value in repo_digests)):
|
||||
raise ValidationError(
|
||||
f"image must be linux/amd64 with an ID and repository digest; got {platform}, "
|
||||
f"image must be linux/amd64 with a content-addressed ID; got {platform}, "
|
||||
f"id={image_id!r}, digests={repo_digests!r}"
|
||||
)
|
||||
return image_id, repo_digests
|
||||
|
||||
|
||||
def immutable_image_reference(image_id: str, repo_digests: Sequence[str]) -> str:
|
||||
"""Prefer a registry digest, falling back to Docker's content-addressed image ID."""
|
||||
if not IMAGE_ID.fullmatch(image_id):
|
||||
raise ValidationError(f"invalid Docker image ID: {image_id!r}")
|
||||
if any(not REPO_DIGEST.fullmatch(value) for value in repo_digests):
|
||||
raise ValidationError("invalid Docker repository digest")
|
||||
return sorted(repo_digests)[0] if repo_digests else image_id
|
||||
|
||||
|
||||
def is_immutable_image_reference(value: str) -> bool:
|
||||
return bool(IMAGE_ID.fullmatch(value) or REPO_DIGEST.fullmatch(value))
|
||||
|
||||
|
||||
def _docker(
|
||||
args: Sequence[str], environ: Mapping[str, str], run: Runner = subprocess.run, **kwargs: Any
|
||||
) -> subprocess.CompletedProcess[str]:
|
||||
@@ -286,8 +302,8 @@ def run_fresh_grader(
|
||||
"""
|
||||
if environ.get("DOCKER_HOST") != REMOTE_DOCKER_HOST:
|
||||
raise ValidationError(f"fresh grader requires DOCKER_HOST={REMOTE_DOCKER_HOST}")
|
||||
if "@sha256:" not in image:
|
||||
raise ValidationError("fresh grader image must be an inspected repository digest")
|
||||
if not is_immutable_image_reference(image):
|
||||
raise ValidationError("fresh grader image must be an inspected immutable reference")
|
||||
spec = swebench_test_spec(record)
|
||||
eval_script = spec.eval_script
|
||||
executed_script, monitored_indices = instrument_eval_script(spec.eval_script_list)
|
||||
@@ -377,7 +393,9 @@ def run_trial(
|
||||
output_path = out_dir / f"{split}-{mode}.txt"
|
||||
model_patch = str(record["patch"]) if mode == "oracle" else ""
|
||||
tested, combined, statuses, eval_script_sha256, eval_script = run_fresh_grader(
|
||||
record, model_patch=model_patch, image=repo_digests[0], environ=environ, run=run,
|
||||
record, model_patch=model_patch,
|
||||
image=immutable_image_reference(image_id, repo_digests),
|
||||
environ=environ, run=run,
|
||||
memory=memory, timeout_seconds=timeout_seconds,
|
||||
)
|
||||
output_path.write_text(combined)
|
||||
@@ -459,7 +477,11 @@ def manifest(
|
||||
if len(identities) != 1:
|
||||
raise ValidationError("manifest cannot record divergent remote image identities")
|
||||
image_id, repo_digests = next(iter(identities))
|
||||
remote_image = {"id": image_id, "repo_digests": list(repo_digests)}
|
||||
remote_image = {
|
||||
"id": image_id,
|
||||
"repo_digests": list(repo_digests),
|
||||
"immutable_ref": immutable_image_reference(image_id, repo_digests),
|
||||
}
|
||||
return {
|
||||
"schema_version": 2,
|
||||
"dataset": DATASET,
|
||||
|
||||
+12
-8
@@ -13,6 +13,10 @@ from messageboardbench.board import MESSAGEBOARD_V2_INTERFACE_VERSION, initializ
|
||||
from messageboardbench.feedback import initialize_feedback
|
||||
|
||||
|
||||
IMAGE_ID = "sha256:" + "a" * 64
|
||||
REPO_DIGEST = "repo@sha256:" + "d" * 64
|
||||
|
||||
|
||||
def records(count=349):
|
||||
return {f"owner__repo-{index:03d}": {"instance_id": f"owner__repo-{index:03d}",
|
||||
"value": index}
|
||||
@@ -119,11 +123,11 @@ def test_compose_has_no_mount_and_network_none():
|
||||
def test_write_compose_uses_validated_digest_override(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(module, "swebench_spec", lambda record: ("repo:latest", [], "pytest"))
|
||||
path = module.write_compose(
|
||||
{"instance_id": "task"}, tmp_path, image_override="repo@sha256:validated"
|
||||
{"instance_id": "task"}, tmp_path, image_override=REPO_DIGEST
|
||||
)
|
||||
assert "repo@sha256:validated" in path.read_text()
|
||||
assert REPO_DIGEST in path.read_text()
|
||||
assert "repo:latest" not in path.read_text()
|
||||
with pytest.raises(ValueError, match="repository digest"):
|
||||
with pytest.raises(ValueError, match="immutable image reference"):
|
||||
module.write_compose({"instance_id": "other"}, tmp_path, image_override="repo:latest")
|
||||
|
||||
|
||||
@@ -134,10 +138,10 @@ def test_sample_binds_fresh_grader_to_validated_digest(tmp_path):
|
||||
"instance_id": "task", "problem_statement": "fix it", "test_patch": "patch"
|
||||
}
|
||||
sample = module.sample_from_record(
|
||||
value, compose, grader_image="repo@sha256:validated"
|
||||
value, compose, grader_image=IMAGE_ID
|
||||
)
|
||||
assert sample.metadata["messageboardbench_grader_image"] == "repo@sha256:validated"
|
||||
with pytest.raises(ValueError, match="repository digest"):
|
||||
assert sample.metadata["messageboardbench_grader_image"] == IMAGE_ID
|
||||
with pytest.raises(ValueError, match="immutable image reference"):
|
||||
module.sample_from_record(value, compose, grader_image="repo:latest")
|
||||
|
||||
|
||||
@@ -237,7 +241,7 @@ def test_paid_scorer_restores_tests_and_uses_fresh_digest_grader(
|
||||
state = SimpleNamespace(
|
||||
metadata={
|
||||
"_messageboardbench_evaluator_commit": "a" * 40,
|
||||
"messageboardbench_grader_image": "repo@sha256:validated",
|
||||
"messageboardbench_grader_image": IMAGE_ID,
|
||||
"test_patch": "--- a/tests/test_x.py\n+++ b/tests/test_x.py\n",
|
||||
"base_commit": "b" * 40,
|
||||
"FAIL_TO_PASS": ["target"], "PASS_TO_PASS": [],
|
||||
@@ -252,7 +256,7 @@ def test_paid_scorer_restores_tests_and_uses_fresh_digest_grader(
|
||||
score = asyncio.run(invocation)
|
||||
assert score.value == 1.0
|
||||
assert captured["model_patch"] == "model patch"
|
||||
assert captured["image"] == "repo@sha256:validated"
|
||||
assert captured["image"] == IMAGE_ID
|
||||
assert captured["memory"] == "9g" and captured["timeout_seconds"] == 77
|
||||
assert score.metadata["test_modified_ever"] is True
|
||||
assert score.metadata["grader_container_fresh"] is True
|
||||
|
||||
@@ -21,6 +21,10 @@ from scripts.validate_swe_population_prerequisites import (
|
||||
)
|
||||
|
||||
|
||||
IMAGE_ID = "sha256:" + "a" * 64
|
||||
REPO_DIGEST = "repo@sha256:" + "d" * 64
|
||||
|
||||
|
||||
def write(path, value):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(value if isinstance(value, str) else json.dumps(value))
|
||||
@@ -52,7 +56,7 @@ def fixture(tmp_path):
|
||||
eval_hash = write(tmp_path / "evidence" / eval_name, eval_text)
|
||||
cells.append({"split": split, "mode": mode, "resolved": expected[split, mode],
|
||||
"image": "repo:tag", "test_command": ["pytest"],
|
||||
"image_id": "sha256:image", "repo_digests": ["repo@sha256:digest"],
|
||||
"image_id": IMAGE_ID, "repo_digests": [REPO_DIGEST],
|
||||
"grader_container_fresh": True,
|
||||
"eval_script_sha256": eval_hash, "eval_script_file": eval_name,
|
||||
"model_patch_sha256": (
|
||||
@@ -69,8 +73,8 @@ def fixture(tmp_path):
|
||||
"grader_isolation": "fresh-container-per-scoring-attempt",
|
||||
"grading_lifecycle": prerequisites_module.GRADING_LIFECYCLE,
|
||||
"image": "repo:tag",
|
||||
"remote_image": {"id": "sha256:image",
|
||||
"repo_digests": ["repo@sha256:digest"]},
|
||||
"remote_image": {"id": IMAGE_ID, "repo_digests": [REPO_DIGEST],
|
||||
"immutable_ref": REPO_DIGEST},
|
||||
"test_command": ["pytest"],
|
||||
"base_commit": "base", "repo": "org/repo", "version": "1",
|
||||
"original_test_patch_sha256": hashlib.sha256(b"original").hexdigest(),
|
||||
@@ -116,6 +120,28 @@ def test_environment_index_is_required_and_plan_bound(tmp_path):
|
||||
validate_environment_index(plan, tmp_path)
|
||||
|
||||
|
||||
def test_environment_index_accepts_content_addressed_local_image_without_repo_digest(
|
||||
tmp_path,
|
||||
):
|
||||
plan, manifest_path, record = fixture(tmp_path)
|
||||
manifest = json.loads(manifest_path.read_text())
|
||||
manifest["remote_image"] = {
|
||||
"id": IMAGE_ID, "repo_digests": [], "immutable_ref": IMAGE_ID,
|
||||
}
|
||||
for row in manifest["results"]:
|
||||
row["repo_digests"] = []
|
||||
manifest_hash = write(manifest_path, manifest)
|
||||
index_path = tmp_path / "index.json"
|
||||
index = json.loads(index_path.read_text())
|
||||
index["manifests"]["task"]["sha256"] = manifest_hash
|
||||
write(index_path, index)
|
||||
|
||||
evidence = validate_environment_index_for_records(plan, tmp_path, {"task": record})
|
||||
selected = evidence["validated_instances"][0]
|
||||
assert selected["validated_image_ref"] == IMAGE_ID
|
||||
assert selected["validated_repo_digest"] is None
|
||||
|
||||
|
||||
def test_unresolved_cell_requires_an_actual_failed_target(tmp_path):
|
||||
plan, manifest_path, record = fixture(tmp_path)
|
||||
manifest = json.loads(manifest_path.read_text())
|
||||
|
||||
@@ -10,6 +10,10 @@ import pytest
|
||||
from messageboardbench import swe_validation as module
|
||||
|
||||
|
||||
IMAGE_ID = "sha256:" + "a" * 64
|
||||
REPO_DIGEST = "repo@sha256:" + "d" * 64
|
||||
|
||||
|
||||
def record(**changes):
|
||||
value = {
|
||||
"instance_id": "owner__repo-1",
|
||||
@@ -34,8 +38,8 @@ def result(split: str, mode: str, *, resolved: bool, exit_code: int):
|
||||
output_file=f"{split}-{mode}.txt",
|
||||
output_sha256="0" * 64,
|
||||
image="swebench/sweb.eval.x86_64.example:latest",
|
||||
image_id="sha256:abc",
|
||||
repo_digests=["swebench/example@sha256:def"],
|
||||
image_id=IMAGE_ID,
|
||||
repo_digests=[REPO_DIGEST],
|
||||
test_command=["pytest", "tests/test_x.py"],
|
||||
target_statuses={"tests/test_x.py::test_bug": "PASSED" if resolved else "FAILED"},
|
||||
resolved=resolved,
|
||||
@@ -115,18 +119,32 @@ def test_matrix_rejects_image_identity_drift():
|
||||
def test_image_identity_requires_digest_and_amd64():
|
||||
def run(command, **kwargs):
|
||||
payload = {
|
||||
"Id": "sha256:abc",
|
||||
"RepoDigests": ["repo@sha256:def"],
|
||||
"Id": IMAGE_ID,
|
||||
"RepoDigests": [REPO_DIGEST],
|
||||
"Os": "linux",
|
||||
"Architecture": "amd64",
|
||||
}
|
||||
return subprocess.CompletedProcess(command, 0, __import__("json").dumps(payload), "")
|
||||
|
||||
assert module.image_identity("repo:tag", {"DOCKER_HOST": module.REMOTE_DOCKER_HOST}, run) == (
|
||||
"sha256:abc", ["repo@sha256:def"]
|
||||
IMAGE_ID, [REPO_DIGEST]
|
||||
)
|
||||
|
||||
|
||||
def test_image_identity_accepts_local_content_address_without_repo_digest():
|
||||
def run(command, **kwargs):
|
||||
payload = {
|
||||
"Id": IMAGE_ID, "RepoDigests": [], "Os": "linux", "Architecture": "amd64",
|
||||
}
|
||||
return subprocess.CompletedProcess(command, 0, __import__("json").dumps(payload), "")
|
||||
|
||||
identity = module.image_identity(
|
||||
"local:tag", {"DOCKER_HOST": module.REMOTE_DOCKER_HOST}, run
|
||||
)
|
||||
assert identity == (IMAGE_ID, [])
|
||||
assert module.immutable_image_reference(*identity) == IMAGE_ID
|
||||
|
||||
|
||||
def test_semantic_audit_is_bound_to_pair_hashes():
|
||||
expected = {
|
||||
"dataset": module.DATASET,
|
||||
@@ -198,7 +216,7 @@ def test_fresh_grader_runs_exact_testspec_script_with_install_and_network_none(m
|
||||
return subprocess.CompletedProcess(command, 0, stdout, "")
|
||||
|
||||
evaluated, output, statuses, script_hash, preserved_script = module.run_fresh_grader(
|
||||
record(), model_patch="diff --git a/x b/x\n", image="repo@sha256:digest",
|
||||
record(), model_patch="diff --git a/x b/x\n", image=REPO_DIGEST,
|
||||
environ={"DOCKER_HOST": module.REMOTE_DOCKER_HOST}, run=run,
|
||||
)
|
||||
assert evaluated.returncode == 0
|
||||
|
||||
@@ -103,6 +103,7 @@ def test_environment_validation_uses_preserved_snapshot(tmp_path, monkeypatch):
|
||||
"manifest_sha256": digest(archived_manifest),
|
||||
"validated_image": "image",
|
||||
"validated_image_id": "image-id",
|
||||
"validated_image_ref": "repo-digest",
|
||||
"validated_repo_digest": "repo-digest",
|
||||
}],
|
||||
}
|
||||
@@ -110,7 +111,8 @@ def test_environment_validation_uses_preserved_snapshot(tmp_path, monkeypatch):
|
||||
from messageboardbench import swe_prerequisites
|
||||
monkeypatch.setattr(swe_prerequisites, "validate_task_manifest", lambda *args, **kwargs: {
|
||||
"image": "image",
|
||||
"remote_image": {"id": "image-id", "repo_digests": ["repo-digest"]},
|
||||
"remote_image": {"id": "image-id", "repo_digests": ["repo-digest"],
|
||||
"immutable_ref": "repo-digest"},
|
||||
})
|
||||
|
||||
assert matches(manifest, frozen, tmp_path / "run")
|
||||
|
||||
Reference in new issue
Block a user