pluginbench 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pluginbench/__init__.py +3 -0
- pluginbench/__main__.py +3 -0
- pluginbench/assessment-charts.html +790 -0
- pluginbench/assessment.py +316 -0
- pluginbench/assessment_charts.py +1225 -0
- pluginbench/assessment_metrics.py +1956 -0
- pluginbench/assessment_models.py +127 -0
- pluginbench/baselines.py +792 -0
- pluginbench/claude.py +374 -0
- pluginbench/cli.py +672 -0
- pluginbench/code_diff.py +207 -0
- pluginbench/codex.py +451 -0
- pluginbench/codex_events.py +585 -0
- pluginbench/collectors.py +591 -0
- pluginbench/compare.py +927 -0
- pluginbench/components.py +129 -0
- pluginbench/coverage_report.py +610 -0
- pluginbench/datasets.py +207 -0
- pluginbench/evaluation.example.yaml +45 -0
- pluginbench/evaluation.py +1651 -0
- pluginbench/evaluation_report.py +1004 -0
- pluginbench/fallow_parse.mjs +101 -0
- pluginbench/harbor_backend.py +2946 -0
- pluginbench/isolation.py +314 -0
- pluginbench/judge.py +683 -0
- pluginbench/language_analysis.py +915 -0
- pluginbench/ledger.py +217 -0
- pluginbench/measurement_profiles.py +616 -0
- pluginbench/native_treatment_runtime.js +221 -0
- pluginbench/native_treatments.py +155 -0
- pluginbench/observer_data/go-analysis-v1/go.mod +10 -0
- pluginbench/observer_data/go-analysis-v1/go.sum +10 -0
- pluginbench/observer_data/go-analysis-v1/main.go +64 -0
- pluginbench/observer_data/oxc-parser-0.126.0/package-lock.json +449 -0
- pluginbench/observer_data/oxc-parser-0.126.0/package.json +5 -0
- pluginbench/parallel.py +101 -0
- pluginbench/processes.py +100 -0
- pluginbench/remeasure.py +992 -0
- pluginbench/run_lock.py +227 -0
- pluginbench/run_status.py +518 -0
- pluginbench/study.py +1858 -0
- pluginbench/study_review.py +924 -0
- pluginbench/submissions.py +486 -0
- pluginbench/task_sources.py +403 -0
- pluginbench/treatments.py +388 -0
- pluginbench/trial_containers.py +94 -0
- pluginbench/vendor/ECHARTS-LICENSE +222 -0
- pluginbench/vendor/ECHARTS-NOTICE +5 -0
- pluginbench/vendor/ZRENDER-LICENSE +29 -0
- pluginbench/vendor/echarts.min.js +48 -0
- pluginbench-0.5.0.dist-info/METADATA +294 -0
- pluginbench-0.5.0.dist-info/RECORD +58 -0
- pluginbench-0.5.0.dist-info/WHEEL +4 -0
- pluginbench-0.5.0.dist-info/entry_points.txt +2 -0
- pluginbench-0.5.0.dist-info/licenses/LICENSE +201 -0
- pluginbench-0.5.0.dist-info/licenses/src/pluginbench/vendor/ECHARTS-LICENSE +222 -0
- pluginbench-0.5.0.dist-info/licenses/src/pluginbench/vendor/ECHARTS-NOTICE +5 -0
- pluginbench-0.5.0.dist-info/licenses/src/pluginbench/vendor/ZRENDER-LICENSE +29 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
"""Canonical source for explicitly reviewed, patch-grading native contracts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import tempfile
|
|
12
|
+
import tomllib
|
|
13
|
+
from pathlib import Path, PurePosixPath
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from pluginbench.assessment_metrics import EXCLUDED_NAMES, inspect_snapshot
|
|
17
|
+
from pluginbench.code_diff import _file_changes
|
|
18
|
+
|
|
19
|
+
SOURCE_POLICY = "reviewed-native-patch-source-v2"
|
|
20
|
+
GRADER_STARTUP_POLICY = "reviewed-native-grader-startup-v1"
|
|
21
|
+
_GRADER_ENVIRONMENT = {
|
|
22
|
+
"APP_DIR": "/app",
|
|
23
|
+
"TESTS_DIR": "/tests",
|
|
24
|
+
"ARTIFACTS_DIR": "/logs/artifacts",
|
|
25
|
+
"VERIFIER_DIR": "/logs/verifier",
|
|
26
|
+
}
|
|
27
|
+
_GRADER_SHA256 = "47cc9eaadf21e636323c360ec4fa786f0733ec9fd1d21ea5a5717ff9f8c4077c"
|
|
28
|
+
# Reviewed complete verifier trees, including task entrypoint, config and hidden patch.
|
|
29
|
+
# A shared grader filename or even its fingerprint alone is insufficient admission.
|
|
30
|
+
_REVIEWED_TESTS = {
|
|
31
|
+
"ac4aadf99cfbda751fb12f9faef03cc6a3ccbeed8cf1ccda9441c811ef247acc": "d8fbebc75de4991a32354518beff1abf628d0b07",
|
|
32
|
+
"9caf86b1898a4e1f9077e7ea9ed7f7e46c16bec89526916cf56a1d50187acfb5": "88e3d965c0b1628642a30a841745b410d6835052",
|
|
33
|
+
"0eaf29b862c00fe04c8d5f76b63297fd15e8193da13b548f9a68e57b8ccf276d": "a691069fcadf9131e5f7a5a130a022dc678f3e1d",
|
|
34
|
+
"5712990cdc674f4a167fc86ac9104daa7070cadbd051eeb329589a0fa1b174db": "c981f9c7aa91b83cc9bd33a09ecee9e751b06e8d",
|
|
35
|
+
# happy-dom-abort-pending-body-reads and testem-per-launcher-reports at DeepSWE 0b9fabbb.
|
|
36
|
+
"9d5f5c01aeb6935be963a7858beeacc97f231b92ccd34925a94ba5a46454673c": "82a0888cb2c87a6123e05424b528f8e8c9b3e426",
|
|
37
|
+
"eda69e4b08e0976c74e59f4556e97ac0721b0ff0e154f58b6c3ec20b52249f10": "158f61ea91c9613d2011c41ee9be40ada1d7a307",
|
|
38
|
+
# go-critic-doc-link-checker at DeepSWE 0b9fabbb.
|
|
39
|
+
"7aadc441e56f0c2a29a10e560a04720c2e41a71751b67d2a93981ce9edcbdcb1": "9aea378c4dccd6f4394196ad8f0873b3e84678c8",
|
|
40
|
+
# oxvg-structural-selector-preservation at DeepSWE 0b9fabbb.
|
|
41
|
+
"f34b8dfebb3f2a53d473f0b7914a4afe8fae1ca6003fbd0b12ed80dd1cdd5f4d": "1fd7fab851ecc975e008be0e3e279568ce4e2b51",
|
|
42
|
+
# termenv-preserve-ansi-resets at DeepSWE 0b9fabbb.
|
|
43
|
+
"02d9f47229ee4e271c31e627484f0182ed0a37ad75ee2af65590f6817a640bc1": "368a3572b8146cc038b3f240da6792003d7e42c5",
|
|
44
|
+
# ts-pattern-match-each at DeepSWE 0b9fabbb.
|
|
45
|
+
"5ac2f376722eda2df307bfa4c86ba1fd3e93267928a212da5840c9f7f11f851e": "f66fc061fde4f764b113ededa09be63dae564159",
|
|
46
|
+
# cattrs-partial-structuring-recovery at DeepSWE 0b9fabbb.
|
|
47
|
+
"62c19e25862ba6465d0fba5be783857e4b874457850813b5d8fe588146d3e70c": "6bc4708fb9b2ac52d9a18997e923da6a58916102",
|
|
48
|
+
# skrub-duration-encoding at DeepSWE 0b9fabbb.
|
|
49
|
+
"6a75f354bd229f0f45b7b51ee9fb74fb6d40de576ae9ec707a870cfeafd8879c": "24c4466fea94f551fb73d21eba54038dc5b346d3",
|
|
50
|
+
# wasmi-trap-coredumps at DeepSWE 0b9fabbb.
|
|
51
|
+
"b93c2efb48680988a72a0c617ec66d81b0e4d9ccd9a5432052183011d9bd12d9": "e1f76e285b9ad68a952b7cf5297bbb7ab91e6028",
|
|
52
|
+
# fd-deterministic-multi-key-sorting at DeepSWE 0b9fabbb.
|
|
53
|
+
"29a16328fb01fb1deec3abde7c841279169ac5eca614d9293131f5261f055390": "227883606023d62275fb48701aeac90f2b604143",
|
|
54
|
+
# boa-hierarchical-evaluation-cancellation at DeepSWE 0b9fabbb.
|
|
55
|
+
"70754686d59a946aa5823e134462a23b300687db4e4b7bffe756258fa667a959": "70409a5052984325dccfdc5f6520818568a81f39",
|
|
56
|
+
# tengo-destructuring-bindings at DeepSWE 0b9fabbb.
|
|
57
|
+
"277fcdba6cd4102362294c1f44fda1fe6fdd7a7bb6fe1bfc7b8bb6e6401fab66": "3cad0da7a51b1206c6f01e3f4fbb44b976d5275c",
|
|
58
|
+
# tomlkit-toml-table-converters at DeepSWE 0b9fabbb.
|
|
59
|
+
"263d0a5fb6423b68487bed4a9de25c1cfeb73960732dcd33534ef9615c15cc8d": "dd05eebc8ed9e30fc6c223088a5a450cb54c1cab",
|
|
60
|
+
# abs-stepped-slices at DeepSWE 0b9fabbb.
|
|
61
|
+
"39effb9fa65a618d116f608e75b3e49c220b58e9a4441f3329f28c8d2ea9a7b7": "cb1b3b671d0ee9fa9da9f7b02f86967953ffd10a",
|
|
62
|
+
# expr-try-catch-errors at DeepSWE 0b9fabbb.
|
|
63
|
+
"98c702c7f932c4407d44448022978af1d5cb73cd1bf8cc80dd8bbbb77a9d2475": "851b241a301f7c74646e65e4009c69cf290993a8",
|
|
64
|
+
# kysely-window-grouping-helpers at DeepSWE 0b9fabbb.
|
|
65
|
+
"a9ced82977aea08738b3d6b865be2cd1cc63709f31aa872c9d23854f6d40f10f": "91cf3733b2a419f5b17dff118cedb7052ab5300d",
|
|
66
|
+
# actionlint-action-pinning-lint at DeepSWE 0b9fabbb.
|
|
67
|
+
"5762d47f0d98aebf38297b3fc166b270237e94e72e02a9b588b3bbf9d7ca6d8b": "0bdc95715fa58f64e3fd6e63b0f89be8733cbbab",
|
|
68
|
+
# etree-xml-diff-patch at DeepSWE 0b9fabbb.
|
|
69
|
+
"cc9c0afada1f2fb84ef17c9ad7832090d385fc8803e5b1b4f0d476ccae84a1d3": "4032e04c8f2e2f35e43ce5d772fcef14a5df4d74",
|
|
70
|
+
# returns-validated-error-accumulation at DeepSWE 0b9fabbb.
|
|
71
|
+
"e92a9be1e52f9fb15aef1d6264122298a88c5d9f9c80d68fe2fcf2cd48fb424b": "41607fae1289de2787523c452d75212206b9c7c0",
|
|
72
|
+
# anko-default-function-arguments at DeepSWE 0b9fabbb.
|
|
73
|
+
"7b9bfaaa953eef4e6052b163b2004dc18f072f0b9b465348ba1976db9ba050d5": "9d2d84bb1564e9513287998c56ccf16c01c19008",
|
|
74
|
+
# textual-richlog-follow-state at DeepSWE 0b9fabbb.
|
|
75
|
+
"078133321f346427078548e9f0f21aa45b0b28d3ead4fc6df918dfd5522ace51": "0f0849fd37fbd0d4d6f81889476c22340129df67",
|
|
76
|
+
# bandit-structured-nosec-directives at DeepSWE 0b9fabbb.
|
|
77
|
+
"ad33fe4854693f57f9ba084d7e4443e9513524051ea8401e1f2ea778ab86dcc4": "b46fa3a2723635aa29cc012538df4867ac2ac006",
|
|
78
|
+
# anko-typed-variable-bindings at DeepSWE 0b9fabbb.
|
|
79
|
+
"ed5a0a7465b87cdeedc4e27e5d68c8daf6e399495da52364868e59cb4fe42ea9": "3f269a72ff69398b1250c584171f32d12c0d8085",
|
|
80
|
+
# yjs-map-conflict-detection at DeepSWE 0b9fabbb.
|
|
81
|
+
"656e5337e72b1e03aa3f870f99d8387caad74613fc8eb157e9d09ce8933761f1": "7795050a749bd1111cbbdd9d0219b27226a8e710",
|
|
82
|
+
# sql-formatter-bigquery-pipe-formatting at DeepSWE 0b9fabbb.
|
|
83
|
+
"2d470eedde92f4ce193c895b3eb851dd794ff188d6321e77c70756d7b471c7dc": "954e5a474b9e3d45ca58f02a3a4eac8e1947acc5",
|
|
84
|
+
}
|
|
85
|
+
PATCH_ARTIFACT = "logs/artifacts/model.patch"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _digest(value: object) -> str:
|
|
89
|
+
return hashlib.sha256(json.dumps(value, sort_keys=True).encode()).hexdigest()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def portable_qualification(proof: object) -> dict[str, object]:
|
|
93
|
+
"""Bind source semantics without container IDs, cleanup timestamps or run locations."""
|
|
94
|
+
if not isinstance(proof, dict):
|
|
95
|
+
return {"status": "unknown", "reason": "legacy preparation"}
|
|
96
|
+
cleanup = proof.get("cleanup")
|
|
97
|
+
return {
|
|
98
|
+
**{
|
|
99
|
+
key: value
|
|
100
|
+
for key, value in proof.items()
|
|
101
|
+
if key not in {"base_tree", "export", "cleanup"}
|
|
102
|
+
},
|
|
103
|
+
"cleanup_status": cleanup.get("status") if isinstance(cleanup, dict) else "unavailable",
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def grader_startup(*, config: object, image_id: str) -> dict[str, object]:
|
|
108
|
+
"""Admit the startup semantics Harbor actually inherits from a prebuilt grader."""
|
|
109
|
+
if not isinstance(config, dict):
|
|
110
|
+
raise ValueError("native grader image configuration unavailable")
|
|
111
|
+
if config.get("Entrypoint") not in (None, []):
|
|
112
|
+
raise ValueError("native grader image inherited entrypoint is unsupported")
|
|
113
|
+
if config.get("WorkingDir") != "/app":
|
|
114
|
+
raise ValueError("native grader image workdir differs from reviewed /app contract")
|
|
115
|
+
variables = config.get("Env")
|
|
116
|
+
if variables is None:
|
|
117
|
+
variables = []
|
|
118
|
+
if not isinstance(variables, list) or not all(isinstance(value, str) for value in variables):
|
|
119
|
+
raise ValueError("native grader image environment unavailable")
|
|
120
|
+
environment: dict[str, str] = {}
|
|
121
|
+
for variable in variables:
|
|
122
|
+
key, separator, value = variable.partition("=")
|
|
123
|
+
if key in _GRADER_ENVIRONMENT:
|
|
124
|
+
if not separator or key in environment or value != _GRADER_ENVIRONMENT[key]:
|
|
125
|
+
raise ValueError("native grader image redirects reviewed environment: " + key)
|
|
126
|
+
environment[key] = value
|
|
127
|
+
return {
|
|
128
|
+
"policy": GRADER_STARTUP_POLICY,
|
|
129
|
+
"image_id": image_id,
|
|
130
|
+
"workdir": "/app",
|
|
131
|
+
"entrypoint": [],
|
|
132
|
+
"environment": environment,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def reviewed_contract(task_dir: Path) -> dict[str, object]:
|
|
137
|
+
"""Admit only the reviewed verifier and exact artifact collection semantics."""
|
|
138
|
+
from pluginbench.harbor_backend import native_task_assets
|
|
139
|
+
|
|
140
|
+
native_task_assets(task_dir)
|
|
141
|
+
tests = task_dir / "tests"
|
|
142
|
+
tests_digest = native_task_assets(tests)
|
|
143
|
+
base = _REVIEWED_TESTS.get(tests_digest)
|
|
144
|
+
if base is None:
|
|
145
|
+
raise ValueError("unsupported native patch grading contract: verifier tree unreviewed")
|
|
146
|
+
if hashlib.sha256((tests / "grader.py").read_bytes()).hexdigest() != _GRADER_SHA256:
|
|
147
|
+
raise ValueError("unsupported native patch grading contract: grader changed")
|
|
148
|
+
config = json.loads((tests / "config.json").read_text())
|
|
149
|
+
if config.get("base_commit") != base:
|
|
150
|
+
raise ValueError("reviewed verifier base commit changed")
|
|
151
|
+
task = tomllib.loads((task_dir / "task.toml").read_text())
|
|
152
|
+
verifier = task.get("verifier", {})
|
|
153
|
+
collection = [
|
|
154
|
+
{
|
|
155
|
+
"command": "cd /app && mkdir -p /logs/artifacts && git config --global --add "
|
|
156
|
+
"safe.directory /app && git diff --binary "
|
|
157
|
+
+ base
|
|
158
|
+
+ " HEAD > /logs/artifacts/model.patch",
|
|
159
|
+
"service": "main",
|
|
160
|
+
"timeout_sec": 300.0,
|
|
161
|
+
}
|
|
162
|
+
]
|
|
163
|
+
collect = verifier.get("collect")
|
|
164
|
+
if isinstance(collect, list) and all(isinstance(entry, dict) for entry in collect):
|
|
165
|
+
# Harbor runs a collect command in the main service when the task names none.
|
|
166
|
+
collect = [{"service": "main", **entry} for entry in collect]
|
|
167
|
+
if (
|
|
168
|
+
task.get("artifacts") != ["/logs/artifacts/model.patch"]
|
|
169
|
+
or verifier.get("environment_mode") != "separate"
|
|
170
|
+
or verifier.get("network_mode") != "no-network"
|
|
171
|
+
or collect != collection
|
|
172
|
+
or verifier.get("env", {}) != {}
|
|
173
|
+
or verifier.get("environment", {}).get("env", {}) != {}
|
|
174
|
+
):
|
|
175
|
+
raise ValueError(
|
|
176
|
+
"unsupported native patch grading contract: collection or environment changed"
|
|
177
|
+
)
|
|
178
|
+
payload = {
|
|
179
|
+
name: hashlib.sha256((tests / name).read_bytes()).hexdigest()
|
|
180
|
+
for name in ("test.sh", "test.patch", "grader.py", "config.json")
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
"base_commit": base,
|
|
184
|
+
"tests_sha256": tests_digest,
|
|
185
|
+
"collection": collection,
|
|
186
|
+
"grader_sha256": _GRADER_SHA256,
|
|
187
|
+
"patch_artifact": PATCH_ARTIFACT,
|
|
188
|
+
"verifier_payload": payload,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def base_tree_manifest(output: str) -> dict[str, dict[str, str]]:
|
|
193
|
+
"""Parse finite git ls-tree -r -z BASE output without losing blob identities."""
|
|
194
|
+
entries: dict[str, dict[str, str]] = {}
|
|
195
|
+
for record in output.split("\0"):
|
|
196
|
+
if not record:
|
|
197
|
+
continue
|
|
198
|
+
header, separator, path = record.partition("\t")
|
|
199
|
+
match = re.fullmatch(r"([0-7]{6}) (blob|commit) ([0-9a-f]{40})", header)
|
|
200
|
+
if not separator or match is None or path in entries:
|
|
201
|
+
raise ValueError("invalid or duplicate native base tree entry")
|
|
202
|
+
_safe_path(path)
|
|
203
|
+
entries[path] = {"mode": match[1], "type": match[2], "blob": match[3]}
|
|
204
|
+
if not entries:
|
|
205
|
+
raise ValueError("native base tree inventory is empty")
|
|
206
|
+
return entries
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _safe_path(value: str, *, included: bool = False) -> None:
|
|
210
|
+
path = PurePosixPath(value)
|
|
211
|
+
if (
|
|
212
|
+
not value
|
|
213
|
+
or path.is_absolute()
|
|
214
|
+
or path.as_posix() != value
|
|
215
|
+
or ".." in path.parts
|
|
216
|
+
or "\\" in value
|
|
217
|
+
or any(ord(char) < 32 for char in value)
|
|
218
|
+
or (included and any(part in EXCLUDED_NAMES for part in path.parts))
|
|
219
|
+
):
|
|
220
|
+
raise ValueError("unsafe or excluded native patch path: " + value)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _patch_paths(patch: str) -> set[str]:
|
|
224
|
+
# The reviewed grader does not reliably parse Git's C-quoted paths.
|
|
225
|
+
if any(line.startswith("diff --git ") and '"' in line for line in patch.splitlines()):
|
|
226
|
+
raise ValueError("unsupported quoted native patch path")
|
|
227
|
+
for line in patch.splitlines():
|
|
228
|
+
if re.match(r"(?:old mode|new mode|new file mode|deleted file mode) ", line):
|
|
229
|
+
if line.rsplit(" ", 1)[-1] not in {"100644", "100755"}:
|
|
230
|
+
raise ValueError("unsupported native patch file mode")
|
|
231
|
+
changes = _file_changes(patch)
|
|
232
|
+
paths = {
|
|
233
|
+
path
|
|
234
|
+
for change in changes
|
|
235
|
+
for path in (change.old_path, change.new_path)
|
|
236
|
+
if path is not None
|
|
237
|
+
}
|
|
238
|
+
sections = iter(changes)
|
|
239
|
+
current = None
|
|
240
|
+
for line in patch.splitlines():
|
|
241
|
+
if line.startswith("diff --git "):
|
|
242
|
+
current = next(sections)
|
|
243
|
+
header = _file_changes(line)[0]
|
|
244
|
+
for effective, declared in (
|
|
245
|
+
(current.old_path, header.old_path),
|
|
246
|
+
(current.new_path, header.new_path),
|
|
247
|
+
):
|
|
248
|
+
if effective is not None and effective != declared:
|
|
249
|
+
raise ValueError("inconsistent native patch header and marker paths")
|
|
250
|
+
elif current is not None:
|
|
251
|
+
for prefix, expected in (
|
|
252
|
+
("rename from ", current.old_path),
|
|
253
|
+
("rename to ", current.new_path),
|
|
254
|
+
("copy from ", current.old_path),
|
|
255
|
+
("copy to ", current.new_path),
|
|
256
|
+
):
|
|
257
|
+
if line.startswith(prefix):
|
|
258
|
+
path = line.removeprefix(prefix)
|
|
259
|
+
_safe_path(path, included=True)
|
|
260
|
+
if path != expected:
|
|
261
|
+
raise ValueError("inconsistent native patch rename or copy paths")
|
|
262
|
+
paths.add(path)
|
|
263
|
+
for path in paths:
|
|
264
|
+
_safe_path(path, included=True)
|
|
265
|
+
return paths
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _proof(*, before: Path, task_dir: Path, identity: dict[str, object]) -> dict[str, Any]:
|
|
269
|
+
from pluginbench.harbor_backend import native_task_assets
|
|
270
|
+
|
|
271
|
+
if native_task_assets(task_dir) != identity.get("task_assets_sha256"):
|
|
272
|
+
raise ValueError("frozen native task assets changed")
|
|
273
|
+
contract = reviewed_contract(task_dir)
|
|
274
|
+
proof = identity.get("source_qualification")
|
|
275
|
+
if not isinstance(proof, dict) or proof.get("status") != "complete":
|
|
276
|
+
raise ValueError("native patch source qualification unavailable or incomplete")
|
|
277
|
+
if (
|
|
278
|
+
identity.get("source_policy") != SOURCE_POLICY
|
|
279
|
+
or proof.get("source_policy") != SOURCE_POLICY
|
|
280
|
+
):
|
|
281
|
+
raise ValueError("native patch source policy incompatible")
|
|
282
|
+
if proof.get("contract") != contract or proof.get("grader_head") != contract["base_commit"]:
|
|
283
|
+
raise ValueError("native patch source qualification contract changed")
|
|
284
|
+
if proof.get("grader_tests_sha256") != _digest(contract["verifier_payload"]):
|
|
285
|
+
raise ValueError("native grader image verifier payload unqualified")
|
|
286
|
+
startup = proof.get("grader_startup")
|
|
287
|
+
grader_image = identity.get("grader_image_identity")
|
|
288
|
+
if not isinstance(startup, dict) or not isinstance(grader_image, dict):
|
|
289
|
+
raise ValueError("native grader startup qualification unavailable")
|
|
290
|
+
environment = startup.get("environment")
|
|
291
|
+
if not isinstance(environment, dict) or not all(
|
|
292
|
+
isinstance(key, str) and isinstance(value, str) for key, value in environment.items()
|
|
293
|
+
):
|
|
294
|
+
raise ValueError("native grader startup environment qualification unavailable")
|
|
295
|
+
admitted_startup = grader_startup(
|
|
296
|
+
config={
|
|
297
|
+
"Entrypoint": startup.get("entrypoint"),
|
|
298
|
+
"WorkingDir": startup.get("workdir"),
|
|
299
|
+
"Env": [key + "=" + value for key, value in environment.items()],
|
|
300
|
+
},
|
|
301
|
+
image_id=str(grader_image.get("image_id")),
|
|
302
|
+
)
|
|
303
|
+
if startup != admitted_startup:
|
|
304
|
+
raise ValueError("native grader startup qualification changed")
|
|
305
|
+
for key, proof_key in (
|
|
306
|
+
("image_identity", "coding_image_id"),
|
|
307
|
+
("grader_image_identity", "grader_image_id"),
|
|
308
|
+
):
|
|
309
|
+
image = identity.get(key)
|
|
310
|
+
if not isinstance(image, dict) or image.get("image_id") != proof.get(proof_key):
|
|
311
|
+
raise ValueError("native patch source qualification image changed")
|
|
312
|
+
if (
|
|
313
|
+
inspect_snapshot(before)["digest"] != proof.get("fixture_digest")
|
|
314
|
+
or native_task_assets(before) != proof.get("fixture_metadata_digest")
|
|
315
|
+
or identity.get("source_digest") != proof.get("fixture_digest")
|
|
316
|
+
):
|
|
317
|
+
raise ValueError("frozen native patch fixture changed")
|
|
318
|
+
tree = proof.get("base_tree")
|
|
319
|
+
if not isinstance(tree, dict) or not tree or _digest(tree) != proof.get("base_tree_sha256"):
|
|
320
|
+
raise ValueError("native patch base inventory changed or unavailable")
|
|
321
|
+
cleanup = proof.get("cleanup")
|
|
322
|
+
if not isinstance(cleanup, dict) or cleanup.get("status") != "complete":
|
|
323
|
+
raise ValueError("native patch qualification cleanup incomplete")
|
|
324
|
+
return proof
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def reconstruct(
|
|
328
|
+
*,
|
|
329
|
+
before: Path,
|
|
330
|
+
task_dir: Path,
|
|
331
|
+
identity: dict[str, object],
|
|
332
|
+
artifacts: Path,
|
|
333
|
+
execution: dict[str, Any],
|
|
334
|
+
destination: Path,
|
|
335
|
+
) -> dict[str, object]:
|
|
336
|
+
"""Reconstruct the admitted hash-bound artifact; never substitute a stopped export."""
|
|
337
|
+
from pluginbench.harbor_backend import native_task_assets
|
|
338
|
+
|
|
339
|
+
if destination.exists() or destination.is_symlink():
|
|
340
|
+
raise ValueError("canonical native destination already exists")
|
|
341
|
+
if any(parent.is_symlink() for parent in destination.parents):
|
|
342
|
+
raise ValueError("canonical native destination is linked")
|
|
343
|
+
proof = _proof(before=before, task_dir=task_dir, identity=identity)
|
|
344
|
+
hashes = execution.get("native_artifact_hashes")
|
|
345
|
+
if not isinstance(hashes, dict) or not isinstance(hashes.get(PATCH_ARTIFACT), str):
|
|
346
|
+
raise ValueError("admitted native graded patch hash unavailable")
|
|
347
|
+
patch = artifacts / "harbor/trial/artifacts" / PATCH_ARTIFACT
|
|
348
|
+
if (
|
|
349
|
+
not patch.is_file()
|
|
350
|
+
or patch.is_symlink()
|
|
351
|
+
or any(parent.is_symlink() for parent in patch.parents)
|
|
352
|
+
):
|
|
353
|
+
raise ValueError("native graded patch unavailable or linked")
|
|
354
|
+
patch_bytes = patch.read_bytes()
|
|
355
|
+
digest = hashlib.sha256(patch_bytes).hexdigest()
|
|
356
|
+
if digest != hashes[PATCH_ARTIFACT]:
|
|
357
|
+
raise ValueError("native graded patch changed")
|
|
358
|
+
text = patch_bytes.decode("utf-8", errors="strict")
|
|
359
|
+
paths = _patch_paths(text)
|
|
360
|
+
tree = proof["base_tree"]
|
|
361
|
+
for path in paths:
|
|
362
|
+
preimage = before / path
|
|
363
|
+
entry = tree.get(path)
|
|
364
|
+
if entry is None:
|
|
365
|
+
if preimage.exists():
|
|
366
|
+
raise ValueError("native patch creation collides with frozen fixture: " + path)
|
|
367
|
+
continue
|
|
368
|
+
if (
|
|
369
|
+
not isinstance(entry, dict)
|
|
370
|
+
or entry.get("type") != "blob"
|
|
371
|
+
or entry.get("mode") not in {"100644", "100755"}
|
|
372
|
+
or not preimage.is_file()
|
|
373
|
+
or preimage.is_symlink()
|
|
374
|
+
):
|
|
375
|
+
raise ValueError("native patch preimage unavailable or unsupported: " + path)
|
|
376
|
+
content = preimage.read_bytes()
|
|
377
|
+
blob = hashlib.sha1(b"blob " + str(len(content)).encode() + b"\0" + content).hexdigest()
|
|
378
|
+
mode = "100755" if preimage.stat().st_mode & 0o111 else "100644"
|
|
379
|
+
if blob != entry.get("blob") or mode != entry["mode"]:
|
|
380
|
+
raise ValueError("native patch preimage differs from verifier base: " + path)
|
|
381
|
+
hidden = _patch_paths((task_dir / "tests/test.patch").read_text())
|
|
382
|
+
with tempfile.TemporaryDirectory(prefix="pluginbench-canonical-patch-") as temporary:
|
|
383
|
+
source = Path(temporary) / "source"
|
|
384
|
+
shutil.copytree(before, source)
|
|
385
|
+
try:
|
|
386
|
+
applied = subprocess.run(
|
|
387
|
+
[
|
|
388
|
+
"git",
|
|
389
|
+
"apply",
|
|
390
|
+
"--binary",
|
|
391
|
+
"--allow-empty",
|
|
392
|
+
"--whitespace=nowarn",
|
|
393
|
+
str(patch.resolve()),
|
|
394
|
+
],
|
|
395
|
+
cwd=source,
|
|
396
|
+
capture_output=True,
|
|
397
|
+
text=True,
|
|
398
|
+
check=False,
|
|
399
|
+
timeout=60,
|
|
400
|
+
env={
|
|
401
|
+
"PATH": os.environ["PATH"],
|
|
402
|
+
"GIT_CONFIG_GLOBAL": os.devnull,
|
|
403
|
+
"GIT_CONFIG_NOSYSTEM": "1",
|
|
404
|
+
"GIT_CEILING_DIRECTORIES": temporary,
|
|
405
|
+
},
|
|
406
|
+
)
|
|
407
|
+
except subprocess.TimeoutExpired as error:
|
|
408
|
+
raise ValueError("graded patch reconstruction timed out after 60 seconds") from error
|
|
409
|
+
if applied.returncode:
|
|
410
|
+
raise ValueError("graded patch could not be safely reconstructed: " + applied.stderr)
|
|
411
|
+
source_digest = inspect_snapshot(source)["digest"]
|
|
412
|
+
metadata_digest = native_task_assets(source)
|
|
413
|
+
# Copy only after all validation, and never leave a partially admitted destination.
|
|
414
|
+
try:
|
|
415
|
+
shutil.copytree(source, destination)
|
|
416
|
+
except BaseException:
|
|
417
|
+
if destination.is_dir():
|
|
418
|
+
shutil.rmtree(destination)
|
|
419
|
+
raise
|
|
420
|
+
return {
|
|
421
|
+
"status": "complete",
|
|
422
|
+
"source_policy": SOURCE_POLICY,
|
|
423
|
+
"binding": "reviewed model.patch over qualified frozen fixture",
|
|
424
|
+
"snapshot_digest": source_digest,
|
|
425
|
+
"source_metadata_digest": metadata_digest,
|
|
426
|
+
"patch_artifact": PATCH_ARTIFACT,
|
|
427
|
+
"patch_sha256": digest,
|
|
428
|
+
"preparation_proof_sha256": _digest(portable_qualification(proof)),
|
|
429
|
+
"trusted_test_overlap": sorted(paths & hidden),
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def validate(
|
|
434
|
+
*,
|
|
435
|
+
before: Path,
|
|
436
|
+
task_dir: Path,
|
|
437
|
+
identity: dict[str, object],
|
|
438
|
+
artifacts: Path,
|
|
439
|
+
execution: dict[str, Any],
|
|
440
|
+
snapshot: Path,
|
|
441
|
+
) -> dict[str, object]:
|
|
442
|
+
"""Independently reconstruct and compare the complete canonical snapshot metadata."""
|
|
443
|
+
from pluginbench.harbor_backend import native_task_assets
|
|
444
|
+
|
|
445
|
+
with tempfile.TemporaryDirectory(prefix="pluginbench-validate-patch-") as temporary:
|
|
446
|
+
receipt = reconstruct(
|
|
447
|
+
before=before,
|
|
448
|
+
task_dir=task_dir,
|
|
449
|
+
identity=identity,
|
|
450
|
+
artifacts=artifacts,
|
|
451
|
+
execution=execution,
|
|
452
|
+
destination=Path(temporary).resolve() / "source",
|
|
453
|
+
)
|
|
454
|
+
if (
|
|
455
|
+
inspect_snapshot(snapshot)["digest"] != receipt["snapshot_digest"]
|
|
456
|
+
or native_task_assets(snapshot) != receipt["source_metadata_digest"]
|
|
457
|
+
):
|
|
458
|
+
raise ValueError("canonical graded source changed")
|
|
459
|
+
return receipt
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def validate_export(
|
|
463
|
+
*, diagnostic: Path, artifacts: Path, execution: dict[str, Any]
|
|
464
|
+
) -> dict[str, object]:
|
|
465
|
+
"""Validate stopped-source diagnostics separately from the canonical submission."""
|
|
466
|
+
from pluginbench.harbor_backend import native_task_assets, _native_artifact_hashes
|
|
467
|
+
|
|
468
|
+
receipt_path = artifacts / "submission-source.json"
|
|
469
|
+
for path in (diagnostic, receipt_path, artifacts / "harbor/trial/artifacts"):
|
|
470
|
+
if path.is_symlink() or any(parent.is_symlink() for parent in path.parents):
|
|
471
|
+
raise ValueError("native stopped export evidence is linked")
|
|
472
|
+
receipt = json.loads(receipt_path.read_text())
|
|
473
|
+
if not isinstance(receipt, dict):
|
|
474
|
+
raise ValueError("native stopped export receipt must be an object")
|
|
475
|
+
digest = inspect_snapshot(diagnostic)["digest"]
|
|
476
|
+
if digest != receipt.get("snapshot_digest") or digest != execution.get(
|
|
477
|
+
"submission_export_digest"
|
|
478
|
+
):
|
|
479
|
+
raise ValueError("native stopped export changed")
|
|
480
|
+
if native_task_assets(diagnostic) != receipt.get("source_metadata_digest"):
|
|
481
|
+
raise ValueError("native stopped export metadata changed")
|
|
482
|
+
artifact_dir = artifacts / "harbor/trial/artifacts"
|
|
483
|
+
native_task_assets(artifact_dir)
|
|
484
|
+
if _native_artifact_hashes(artifact_dir) != execution.get("native_artifact_hashes"):
|
|
485
|
+
raise ValueError("native graded artifacts changed")
|
|
486
|
+
return receipt
|