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
pluginbench/isolation.py
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""Constrain local jobs to their owned files, not merely a private HOME."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import platform
|
|
10
|
+
import shutil
|
|
11
|
+
import sys
|
|
12
|
+
import tempfile
|
|
13
|
+
import subprocess
|
|
14
|
+
from functools import lru_cache
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from collections.abc import Iterable
|
|
17
|
+
|
|
18
|
+
from pluginbench.processes import run_command
|
|
19
|
+
|
|
20
|
+
PROTOCOL = "local-filesystem-v1"
|
|
21
|
+
SYSTEM_ROOTS = (
|
|
22
|
+
"/System",
|
|
23
|
+
"/usr/bin",
|
|
24
|
+
"/usr/lib",
|
|
25
|
+
"/usr/libexec",
|
|
26
|
+
"/usr/sbin",
|
|
27
|
+
"/usr/share",
|
|
28
|
+
"/bin",
|
|
29
|
+
"/sbin",
|
|
30
|
+
"/lib",
|
|
31
|
+
"/lib64",
|
|
32
|
+
"/Library/Apple",
|
|
33
|
+
"/Library/Developer",
|
|
34
|
+
"/Library/Keychains",
|
|
35
|
+
"/private/etc",
|
|
36
|
+
"/etc",
|
|
37
|
+
"/dev",
|
|
38
|
+
"/private/var/db",
|
|
39
|
+
"/private/var/run",
|
|
40
|
+
"/private/var/select",
|
|
41
|
+
"/opt/homebrew/Cellar",
|
|
42
|
+
"/opt/homebrew/opt",
|
|
43
|
+
)
|
|
44
|
+
TOOLS = ("codex", "claude", "node", "npm", "pnpm", "fallow", "git", "python3")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class RuntimeDriftError(RuntimeError):
|
|
48
|
+
"""A frozen comparison can no longer admit calls with this runtime."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _roots(paths: Iterable[Path]) -> list[Path]:
|
|
52
|
+
return sorted(
|
|
53
|
+
{
|
|
54
|
+
variant
|
|
55
|
+
for path in paths
|
|
56
|
+
if path.exists()
|
|
57
|
+
for variant in (path.absolute(), path.resolve())
|
|
58
|
+
},
|
|
59
|
+
key=str,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def tool_roots() -> list[Path]:
|
|
64
|
+
roots = [Path(sys.prefix), Path(sys.base_prefix)]
|
|
65
|
+
for name in TOOLS:
|
|
66
|
+
executable = shutil.which(name)
|
|
67
|
+
if executable:
|
|
68
|
+
path = Path(executable).resolve()
|
|
69
|
+
roots.append(Path(executable))
|
|
70
|
+
# Codex code mode spawns a sibling host binary, looked up beside the launched path.
|
|
71
|
+
if name == "codex":
|
|
72
|
+
for directory in {Path(executable).parent, path.parent}:
|
|
73
|
+
host = directory / "codex-code-mode-host"
|
|
74
|
+
if host.is_file():
|
|
75
|
+
roots.append(host)
|
|
76
|
+
# Grant package assets, never an arbitrary executable's parent/home.
|
|
77
|
+
for prefix in (
|
|
78
|
+
Path("/opt/homebrew/Cellar"),
|
|
79
|
+
Path("/opt/homebrew/Caskroom"),
|
|
80
|
+
Path("/usr/local/Cellar"),
|
|
81
|
+
Path("/usr/local/Caskroom"),
|
|
82
|
+
):
|
|
83
|
+
if path.is_relative_to(prefix) and len(path.relative_to(prefix).parts) > 2:
|
|
84
|
+
roots.append(prefix.joinpath(*path.relative_to(prefix).parts[:2]))
|
|
85
|
+
nvm = Path.home() / ".nvm/versions/node"
|
|
86
|
+
if path.is_relative_to(nvm) and len(path.relative_to(nvm).parts) > 1:
|
|
87
|
+
roots.append(nvm / path.relative_to(nvm).parts[0])
|
|
88
|
+
for parent in path.parents:
|
|
89
|
+
if parent.name == "node_modules":
|
|
90
|
+
parts = path.relative_to(parent).parts
|
|
91
|
+
size = 2 if parts[0].startswith("@") else 1
|
|
92
|
+
if len(parts) > size:
|
|
93
|
+
roots.append(parent.joinpath(*parts[:size]))
|
|
94
|
+
break
|
|
95
|
+
for key in ("SSL_CERT_FILE", "SSL_CERT_DIR", "NODE_EXTRA_CA_CERTS"):
|
|
96
|
+
if value := os.environ.get(key):
|
|
97
|
+
roots.append(Path(value))
|
|
98
|
+
return _roots(roots)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def codex_code_mode_host() -> Path:
|
|
102
|
+
"""Statically prove an isolated Codex review can start its code-mode host."""
|
|
103
|
+
executable = shutil.which("codex")
|
|
104
|
+
if executable is None:
|
|
105
|
+
raise ValueError("Codex reviews need codex on PATH")
|
|
106
|
+
host = Path(executable).parent / "codex-code-mode-host"
|
|
107
|
+
if not host.is_file() or not os.access(host, os.X_OK):
|
|
108
|
+
raise ValueError(
|
|
109
|
+
f"Codex reviews need an executable {host}: code mode starts it beside the launched "
|
|
110
|
+
"codex, and every review tool call fails without it"
|
|
111
|
+
)
|
|
112
|
+
roots = tool_roots()
|
|
113
|
+
if host.absolute() not in roots or host.resolve() not in roots:
|
|
114
|
+
raise ValueError(f"The review sandbox does not grant {host}; code mode cannot start")
|
|
115
|
+
return host
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@lru_cache(maxsize=64)
|
|
119
|
+
def _file_hash(path: Path, signature: tuple[int, ...]) -> str:
|
|
120
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _binary_hash(path: Path) -> str:
|
|
124
|
+
stat = path.stat()
|
|
125
|
+
return _file_hash(
|
|
126
|
+
path, (stat.st_dev, stat.st_ino, stat.st_size, stat.st_mtime_ns, stat.st_ctime_ns)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def runtime_identity() -> dict[str, object]:
|
|
131
|
+
system = platform.system()
|
|
132
|
+
backend = (
|
|
133
|
+
Path("/usr/bin/sandbox-exec")
|
|
134
|
+
if system == "Darwin"
|
|
135
|
+
else Path(shutil.which("bwrap") or "/nonexistent/pluginbench-bwrap")
|
|
136
|
+
)
|
|
137
|
+
if system not in {"Darwin", "Linux"} or not backend.is_file():
|
|
138
|
+
raise ValueError("local evaluation requires macOS sandbox-exec or Linux bubblewrap")
|
|
139
|
+
return {
|
|
140
|
+
"protocol": PROTOCOL,
|
|
141
|
+
"implementation_sha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
|
|
142
|
+
"backend": str(backend),
|
|
143
|
+
"backend_sha256": _binary_hash(backend),
|
|
144
|
+
"os": system,
|
|
145
|
+
"os_release": platform.release(),
|
|
146
|
+
"system_read_roots": [str(path) for path in _roots(map(Path, SYSTEM_ROOTS))],
|
|
147
|
+
"tool_read_roots": [str(path) for path in tool_roots()],
|
|
148
|
+
"tools": {
|
|
149
|
+
name: {
|
|
150
|
+
"executable": str(Path(path).resolve()),
|
|
151
|
+
"binary_sha256": _binary_hash(Path(path).resolve()),
|
|
152
|
+
}
|
|
153
|
+
for name in TOOLS
|
|
154
|
+
if (path := shutil.which(name))
|
|
155
|
+
},
|
|
156
|
+
"filesystem": "deny reads and writes outside runtime, selected trusted assets, and owned job roots",
|
|
157
|
+
"network": "agent egress allowed; trusted tests use host network",
|
|
158
|
+
"process_limit": "POSIX process group; macOS does not provide a PID namespace",
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def assert_runtime(expected: dict[str, object]) -> None:
|
|
163
|
+
try:
|
|
164
|
+
actual = runtime_identity()
|
|
165
|
+
except (OSError, ValueError) as error:
|
|
166
|
+
raise RuntimeDriftError("frozen local runtime is no longer available") from error
|
|
167
|
+
if actual != expected:
|
|
168
|
+
raise RuntimeDriftError("local runtime changed after the comparison was frozen")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def referenced_paths(value: object) -> list[Path]:
|
|
172
|
+
"""Existing absolute paths in operator-owned adapter settings."""
|
|
173
|
+
if isinstance(value, dict):
|
|
174
|
+
return [path for item in value.values() for path in referenced_paths(item)]
|
|
175
|
+
if isinstance(value, list):
|
|
176
|
+
return [path for item in value for path in referenced_paths(item)]
|
|
177
|
+
if isinstance(value, str) and value.startswith("/") and Path(value).exists():
|
|
178
|
+
return [Path(value)]
|
|
179
|
+
return []
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def verifier_paths(script: Path) -> list[Path]:
|
|
183
|
+
"""Read dependency declarations without executing the prepared verifier."""
|
|
184
|
+
try:
|
|
185
|
+
nodes = ast.parse(script.read_text()).body
|
|
186
|
+
except SyntaxError:
|
|
187
|
+
return [] # Execution records an invalid verifier as an unavailable check.
|
|
188
|
+
return [
|
|
189
|
+
path
|
|
190
|
+
for node in nodes
|
|
191
|
+
if isinstance(node, ast.Assign)
|
|
192
|
+
and any(isinstance(target, ast.Name) and target.id == "PAYLOAD" for target in node.targets)
|
|
193
|
+
for path in referenced_paths(ast.literal_eval(node.value))
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def command(
|
|
198
|
+
argv: list[str],
|
|
199
|
+
*,
|
|
200
|
+
writable: list[Path],
|
|
201
|
+
readable: Iterable[Path] = (),
|
|
202
|
+
expected: dict[str, object] | None = None,
|
|
203
|
+
) -> tuple[list[str], dict[str, object]]:
|
|
204
|
+
try:
|
|
205
|
+
identity = runtime_identity()
|
|
206
|
+
except (OSError, ValueError) as error:
|
|
207
|
+
if expected is not None:
|
|
208
|
+
raise RuntimeDriftError("frozen local runtime is no longer available") from error
|
|
209
|
+
raise
|
|
210
|
+
if expected is not None and identity != expected:
|
|
211
|
+
raise RuntimeDriftError("local runtime changed before native invocation")
|
|
212
|
+
writes = _roots(writable)
|
|
213
|
+
reads = _roots([*map(Path, SYSTEM_ROOTS), *tool_roots(), *readable, *writes])
|
|
214
|
+
# Metadata traversal does not permit listing or reading sibling directories.
|
|
215
|
+
ancestors = sorted({parent for root in reads for parent in root.parents}, key=str)
|
|
216
|
+
|
|
217
|
+
def quote(path: Path) -> str:
|
|
218
|
+
return json.dumps(str(path), ensure_ascii=True)
|
|
219
|
+
|
|
220
|
+
def path_filter(path: Path) -> str:
|
|
221
|
+
return f"({'literal' if path.is_file() else 'subpath'} {quote(path)})"
|
|
222
|
+
|
|
223
|
+
if platform.system() == "Darwin":
|
|
224
|
+
filters = [f"(require-not {path_filter(path)})" for path in reads]
|
|
225
|
+
data = "\n".join([*filters, '(require-not (literal "/"))'])
|
|
226
|
+
metadata = "\n".join(
|
|
227
|
+
[*filters, *[f"(require-not (literal {quote(path)}))" for path in ancestors]]
|
|
228
|
+
)
|
|
229
|
+
write_filters = "\n".join(f"(subpath {quote(path)})" for path in writes)
|
|
230
|
+
profile = f"""(version 1)
|
|
231
|
+
(allow default)
|
|
232
|
+
(deny file-read-data file-read-xattr (require-all (subpath "/") {data}))
|
|
233
|
+
(deny file-read-metadata (require-all (subpath "/") {metadata}))
|
|
234
|
+
(deny file-write*)
|
|
235
|
+
(allow file-write* {write_filters} (literal "/dev/null") (literal "/dev/stdout") (literal "/dev/stderr") (literal "/dev/tty") (literal "/dev/dtracehelper"))
|
|
236
|
+
(deny process-info*)
|
|
237
|
+
(allow process-info* (target self))
|
|
238
|
+
(deny signal)
|
|
239
|
+
(allow signal (target self))
|
|
240
|
+
(allow signal (target pgrp))
|
|
241
|
+
"""
|
|
242
|
+
wrapped = ["/usr/bin/sandbox-exec", "-p", profile, *argv]
|
|
243
|
+
else:
|
|
244
|
+
wrapped = [
|
|
245
|
+
str(identity["backend"]),
|
|
246
|
+
"--unshare-all",
|
|
247
|
+
"--share-net",
|
|
248
|
+
"--die-with-parent",
|
|
249
|
+
"--new-session",
|
|
250
|
+
"--proc",
|
|
251
|
+
"/proc",
|
|
252
|
+
"--dev",
|
|
253
|
+
"/dev",
|
|
254
|
+
"--tmpfs",
|
|
255
|
+
"/tmp",
|
|
256
|
+
]
|
|
257
|
+
mounted: list[Path] = [Path("/proc"), Path("/dev")]
|
|
258
|
+
for root in sorted(reads, key=lambda path: len(path.parts)):
|
|
259
|
+
if any(root.is_relative_to(parent) for parent in mounted):
|
|
260
|
+
continue
|
|
261
|
+
wrapped.extend(["--ro-bind", str(root), str(root)])
|
|
262
|
+
mounted.append(root)
|
|
263
|
+
for root in writes:
|
|
264
|
+
wrapped.extend(["--bind", str(root), str(root)])
|
|
265
|
+
# Bubblewrap's own root and /tmp are writable scratch by default, so a write outside
|
|
266
|
+
# every granted root would succeed there and vanish. Deny it, as the macOS profile does.
|
|
267
|
+
wrapped.extend(["--remount-ro", "/tmp", "--remount-ro", "/"])
|
|
268
|
+
wrapped.extend(["--", *argv])
|
|
269
|
+
return wrapped, {
|
|
270
|
+
**identity,
|
|
271
|
+
"policy_sha256": hashlib.sha256(json.dumps(wrapped[: -len(argv)]).encode()).hexdigest(),
|
|
272
|
+
"readable": list(map(str, reads)),
|
|
273
|
+
"writable": list(map(str, writes)),
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def run(
|
|
278
|
+
argv: list[str],
|
|
279
|
+
*,
|
|
280
|
+
cwd: Path,
|
|
281
|
+
env: dict[str, str],
|
|
282
|
+
timeout: float,
|
|
283
|
+
readable: Iterable[Path] = (),
|
|
284
|
+
writable: Iterable[Path] = (),
|
|
285
|
+
receipt: Path | None = None,
|
|
286
|
+
) -> subprocess.CompletedProcess[str]:
|
|
287
|
+
"""Execute a trusted adapter and all submitted-code descendants in one boundary."""
|
|
288
|
+
with tempfile.TemporaryDirectory(prefix="pluginbench-runtime-") as temporary:
|
|
289
|
+
private = Path(temporary)
|
|
290
|
+
home, tmp = private / "home", private / "tmp"
|
|
291
|
+
home.mkdir()
|
|
292
|
+
tmp.mkdir()
|
|
293
|
+
arguments = [Path(arg) for arg in argv if arg.startswith("/") and Path(arg).exists()]
|
|
294
|
+
package = Path(__file__).parent
|
|
295
|
+
wrapped, filesystem = command(
|
|
296
|
+
argv,
|
|
297
|
+
writable=[cwd, private, *writable],
|
|
298
|
+
readable=[*readable, *arguments, package.parent],
|
|
299
|
+
)
|
|
300
|
+
if receipt is not None:
|
|
301
|
+
receipt.write_text(json.dumps(filesystem, indent=2) + "\n")
|
|
302
|
+
return run_command(
|
|
303
|
+
wrapped,
|
|
304
|
+
cwd=cwd,
|
|
305
|
+
env={
|
|
306
|
+
**env,
|
|
307
|
+
"HOME": str(home),
|
|
308
|
+
"TMPDIR": str(tmp),
|
|
309
|
+
"XDG_CONFIG_HOME": str(home / ".config"),
|
|
310
|
+
"PYTHONPATH": str(package.parent),
|
|
311
|
+
"PYTHONDONTWRITEBYTECODE": "1",
|
|
312
|
+
},
|
|
313
|
+
timeout=timeout,
|
|
314
|
+
)
|