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.
Files changed (58) hide show
  1. pluginbench/__init__.py +3 -0
  2. pluginbench/__main__.py +3 -0
  3. pluginbench/assessment-charts.html +790 -0
  4. pluginbench/assessment.py +316 -0
  5. pluginbench/assessment_charts.py +1225 -0
  6. pluginbench/assessment_metrics.py +1956 -0
  7. pluginbench/assessment_models.py +127 -0
  8. pluginbench/baselines.py +792 -0
  9. pluginbench/claude.py +374 -0
  10. pluginbench/cli.py +672 -0
  11. pluginbench/code_diff.py +207 -0
  12. pluginbench/codex.py +451 -0
  13. pluginbench/codex_events.py +585 -0
  14. pluginbench/collectors.py +591 -0
  15. pluginbench/compare.py +927 -0
  16. pluginbench/components.py +129 -0
  17. pluginbench/coverage_report.py +610 -0
  18. pluginbench/datasets.py +207 -0
  19. pluginbench/evaluation.example.yaml +45 -0
  20. pluginbench/evaluation.py +1651 -0
  21. pluginbench/evaluation_report.py +1004 -0
  22. pluginbench/fallow_parse.mjs +101 -0
  23. pluginbench/harbor_backend.py +2946 -0
  24. pluginbench/isolation.py +314 -0
  25. pluginbench/judge.py +683 -0
  26. pluginbench/language_analysis.py +915 -0
  27. pluginbench/ledger.py +217 -0
  28. pluginbench/measurement_profiles.py +616 -0
  29. pluginbench/native_treatment_runtime.js +221 -0
  30. pluginbench/native_treatments.py +155 -0
  31. pluginbench/observer_data/go-analysis-v1/go.mod +10 -0
  32. pluginbench/observer_data/go-analysis-v1/go.sum +10 -0
  33. pluginbench/observer_data/go-analysis-v1/main.go +64 -0
  34. pluginbench/observer_data/oxc-parser-0.126.0/package-lock.json +449 -0
  35. pluginbench/observer_data/oxc-parser-0.126.0/package.json +5 -0
  36. pluginbench/parallel.py +101 -0
  37. pluginbench/processes.py +100 -0
  38. pluginbench/remeasure.py +992 -0
  39. pluginbench/run_lock.py +227 -0
  40. pluginbench/run_status.py +518 -0
  41. pluginbench/study.py +1858 -0
  42. pluginbench/study_review.py +924 -0
  43. pluginbench/submissions.py +486 -0
  44. pluginbench/task_sources.py +403 -0
  45. pluginbench/treatments.py +388 -0
  46. pluginbench/trial_containers.py +94 -0
  47. pluginbench/vendor/ECHARTS-LICENSE +222 -0
  48. pluginbench/vendor/ECHARTS-NOTICE +5 -0
  49. pluginbench/vendor/ZRENDER-LICENSE +29 -0
  50. pluginbench/vendor/echarts.min.js +48 -0
  51. pluginbench-0.5.0.dist-info/METADATA +294 -0
  52. pluginbench-0.5.0.dist-info/RECORD +58 -0
  53. pluginbench-0.5.0.dist-info/WHEEL +4 -0
  54. pluginbench-0.5.0.dist-info/entry_points.txt +2 -0
  55. pluginbench-0.5.0.dist-info/licenses/LICENSE +201 -0
  56. pluginbench-0.5.0.dist-info/licenses/src/pluginbench/vendor/ECHARTS-LICENSE +222 -0
  57. pluginbench-0.5.0.dist-info/licenses/src/pluginbench/vendor/ECHARTS-NOTICE +5 -0
  58. pluginbench-0.5.0.dist-info/licenses/src/pluginbench/vendor/ZRENDER-LICENSE +29 -0
@@ -0,0 +1,2946 @@
1
+ """Harbor execution with a stopped-container submission boundary.
2
+
3
+ PluginBench freezes the installed agent image; Harbor runs the agent. The
4
+ workspace is copied only after Docker proves the container is stopped, then
5
+ the native separate verifier grades collected artifacts and owned resources
6
+ are removed. Source and graded-artifact evidence remain distinct.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import copy
13
+ import fcntl
14
+ import hashlib
15
+ import importlib
16
+ import importlib.metadata
17
+ import inspect
18
+ import json
19
+ import os
20
+ import re
21
+ import shlex
22
+ import shutil
23
+ import subprocess
24
+ import sys
25
+ import tempfile
26
+ import time
27
+ from contextlib import ExitStack
28
+ from dataclasses import dataclass
29
+ from pathlib import Path, PurePosixPath
30
+ from typing import Any
31
+ from uuid import uuid4
32
+
33
+ from pluginbench import codex, components, isolation, submissions, native_treatments
34
+ from pluginbench.treatments import Treatment
35
+ from pluginbench.native_treatments import NativeCapabilities
36
+ from pluginbench.code_diff import _is_source_path, _is_test_path
37
+ from pluginbench.assessment_metrics import inspect_snapshot
38
+ from pluginbench.assessment_metrics import EXCLUDED_NAMES
39
+ from pluginbench.processes import run_command
40
+
41
+ HARBOR_VERSION = "0.24.0"
42
+ PROTOCOL = "harbor-docker-v1"
43
+ _OWNER_LABEL = "org.pluginbench.trial"
44
+ _BASE_TOOLS = "curl bash node npm"
45
+ _DOCKER_TIMEOUT = 60
46
+ _CODEX_TRANSPORT_VERSION = "codex-stdout-stderr-v1"
47
+ _CODEX_RUN_SHA256 = "5b14fbcc889a398f80c7b5f7e2a621efc1337ba72e748fafefbab2ad09ad86bd"
48
+ _CODEX_EXEC_PREFIX = (
49
+ "if [ -s ~/.nvm/nvm.sh ]; then . ~/.nvm/nvm.sh; fi; "
50
+ "codex exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check --model "
51
+ )
52
+ _CODEX_EXEC_SUFFIX = "2>&1 </dev/null | tee /logs/agent/codex.txt"
53
+ NATIVE_PROVIDER_HOSTS = {
54
+ "codex": ["api.openai.com", "chatgpt.com", "auth.openai.com"],
55
+ "claude": ["api.anthropic.com", "console.anthropic.com", "claude.ai"],
56
+ }
57
+ NATIVE_TRANSPORT_POLICY = "native-cli-provider-hosts-v1"
58
+ _PREPARATION_POLICY = "native-task-and-qualified-patch-source-v3"
59
+
60
+
61
+ @dataclass(frozen=True)
62
+ class _NativePreparation:
63
+ admission: dict[str, object]
64
+ runtime: dict[str, object]
65
+ base: dict[str, str]
66
+ grader: dict[str, str]
67
+
68
+
69
+ def _assert_codex_transport_source() -> str:
70
+ if importlib.metadata.version("harbor") != HARBOR_VERSION:
71
+ raise ValueError("Codex transport requires the pinned Harbor version")
72
+ agent = importlib.import_module("harbor.agents.installed.codex").Codex
73
+ digest = hashlib.sha256(inspect.getsource(agent.run).encode()).hexdigest()
74
+ if digest != _CODEX_RUN_SHA256:
75
+ raise ValueError("Harbor Codex.run changed; refusing the native transport adapter")
76
+ return digest
77
+
78
+
79
+ def _object(value: object, label: str) -> dict[str, Any]:
80
+ if not isinstance(value, dict) or not all(isinstance(key, str) for key in value):
81
+ raise ValueError(f"{label} must be an object")
82
+ return {key: item for key, item in value.items()}
83
+
84
+
85
+ def _base_inventory_command(base: str) -> str:
86
+ """Shell that prints HEAD and the base tree; on failure it prints git's error instead.
87
+
88
+ `docker logs` returns only the container's standard output, and the inventory must stay free
89
+ of warnings, so git's standard error is held back and printed only when a command fails.
90
+ """
91
+ git = "git -c safe.directory=/app"
92
+ return (
93
+ "set -eu; exec 3>&1; "
94
+ f"error=$({{ {git} rev-parse --verify HEAD >&3 && "
95
+ f"{git} ls-tree -r -z --full-tree {base} >&3; }} 2>&1) "
96
+ '|| { code=$?; printf \'%s\\n\' "$error"; exit "$code"; }'
97
+ )
98
+
99
+
100
+ def _docker(
101
+ *arguments: str,
102
+ timeout: int = _DOCKER_TIMEOUT,
103
+ expected_runtime: dict[str, object] | None = None,
104
+ receipt: Path | None = None,
105
+ ) -> str:
106
+ argv = ["docker"]
107
+ env = dict(os.environ)
108
+ if expected_runtime is not None:
109
+ cli = _object(expected_runtime.get("docker_cli"), "frozen Docker CLI")
110
+ argv = [_frozen_executable(cli, "CLI")["resolved_path"]]
111
+ engine = _object(expected_runtime.get("docker_engine"), "frozen Docker engine")
112
+ endpoint = engine.get("endpoint")
113
+ if not isinstance(endpoint, str) or not endpoint.startswith("unix://"):
114
+ raise ValueError("Frozen Docker endpoint must be a local Unix socket")
115
+ argv.extend(["--host", endpoint])
116
+ env.pop("DOCKER_CONTEXT", None)
117
+ env.pop("DOCKER_HOST", None)
118
+ command = [*argv, *arguments]
119
+ try:
120
+ result = run_command(command, cwd=Path.cwd(), env=env, timeout=timeout)
121
+ except subprocess.TimeoutExpired as error:
122
+ if receipt is not None:
123
+ receipt.parent.mkdir(parents=True, exist_ok=True)
124
+ receipt.write_text(
125
+ json.dumps(
126
+ {
127
+ "command": command,
128
+ "returncode": None,
129
+ "timed_out": True,
130
+ "stdout": (error.output or b"").decode("utf-8", errors="replace"),
131
+ "stderr": (error.stderr or b"").decode("utf-8", errors="replace"),
132
+ },
133
+ sort_keys=True,
134
+ )
135
+ )
136
+ raise
137
+ if receipt is not None:
138
+ receipt.parent.mkdir(parents=True, exist_ok=True)
139
+ receipt.write_text(
140
+ json.dumps(
141
+ {
142
+ "command": command,
143
+ "returncode": result.returncode,
144
+ "timed_out": False,
145
+ "stdout": result.stdout,
146
+ "stderr": result.stderr,
147
+ },
148
+ sort_keys=True,
149
+ )
150
+ )
151
+ if result.returncode:
152
+ raise ValueError(f"docker {arguments[0]} failed: {result.stderr.strip()}")
153
+ return result.stdout
154
+
155
+
156
+ def _assert_daemon(expected_runtime: dict[str, object]) -> None:
157
+ engine = _object(expected_runtime.get("docker_engine"), "frozen Docker engine")
158
+ actual = _object(
159
+ json.loads(_docker("info", "--format", "{{json .}}", expected_runtime=expected_runtime)),
160
+ "Docker daemon",
161
+ )
162
+ if not engine.get("id") or actual.get("ID") != engine["id"]:
163
+ raise ValueError("Docker daemon changed; cannot accept export or cleanup proof")
164
+
165
+
166
+ def _engine() -> tuple[dict[str, Any], str]:
167
+ info = _object(json.loads(_docker("info", "--format", "{{json .}}")), "Docker info")
168
+ contexts = json.loads(_docker("context", "inspect"))
169
+ if not isinstance(contexts, list) or len(contexts) != 1:
170
+ raise ValueError("Docker must have one selected context")
171
+ context = _object(contexts[0], "Docker context")
172
+ endpoints = _object(context.get("Endpoints"), "Docker endpoints")
173
+ docker = _object(endpoints.get("docker"), "Docker endpoint")
174
+ endpoint = (
175
+ docker.get("Host")
176
+ if os.environ.get("DOCKER_CONTEXT")
177
+ else os.environ.get("DOCKER_HOST") or docker.get("Host")
178
+ )
179
+ if not isinstance(endpoint, str) or not endpoint.startswith("unix://"):
180
+ raise ValueError("Harbor execution currently requires a local Unix Docker endpoint")
181
+ return info, endpoint
182
+
183
+
184
+ def _executable_identity(path: Path) -> dict[str, str]:
185
+ resolved = path.resolve(strict=True)
186
+ if not path.is_absolute() or not resolved.is_file() or not os.access(resolved, os.X_OK):
187
+ raise ValueError("Docker executables must be readable absolute executable files")
188
+ with resolved.open("rb") as binary:
189
+ digest = hashlib.file_digest(binary, "sha256").hexdigest()
190
+ return {"path": str(path), "resolved_path": str(resolved), "sha256": digest}
191
+
192
+
193
+ def _docker_plugins(info: dict[str, Any]) -> dict[str, dict[str, str]]:
194
+ client = _object(info.get("ClientInfo"), "Docker client metadata")
195
+ plugins = client.get("Plugins")
196
+ if not isinstance(plugins, list):
197
+ raise ValueError("Docker did not report its CLI plugins")
198
+ result: dict[str, dict[str, str]] = {}
199
+ for name in ("compose", "buildx"):
200
+ selected = [item for item in plugins if isinstance(item, dict) and item.get("Name") == name]
201
+ if len(selected) != 1:
202
+ raise ValueError(f"Docker must report one usable {name} plugin")
203
+ plugin = selected[0]
204
+ path, version = plugin.get("Path"), plugin.get("Version")
205
+ if not isinstance(path, str) or not isinstance(version, str) or not version:
206
+ raise ValueError(f"Docker did not report the {name} executable and version")
207
+ result[name] = {**_executable_identity(Path(path)), "version": version}
208
+ return result
209
+
210
+
211
+ def _docker_cli(info: dict[str, Any]) -> dict[str, str]:
212
+ client = _object(info.get("ClientInfo"), "Docker client metadata")
213
+ version, path = client.get("Version"), shutil.which("docker")
214
+ if not path or not isinstance(version, str) or not version:
215
+ raise ValueError("Docker did not report its client executable and version")
216
+ return {**_executable_identity(Path(path)), "version": version}
217
+
218
+
219
+ def _frozen_executable(identity: dict[str, Any], label: str) -> dict[str, str]:
220
+ try:
221
+ actual = _executable_identity(Path(identity["path"]))
222
+ except (OSError, ValueError) as error:
223
+ raise isolation.RuntimeDriftError(f"Cannot verify frozen Docker {label}") from error
224
+ if any(identity.get(key) != value for key, value in actual.items()):
225
+ raise isolation.RuntimeDriftError(f"Docker {label} changed since preparation")
226
+ return actual
227
+
228
+
229
+ def _private_docker_config(
230
+ *, private: Path, env: dict[str, str], expected_runtime: dict[str, object]
231
+ ) -> dict[str, object]:
232
+ """Expose only frozen plugin binaries, never the operator's Docker settings."""
233
+ plugins = _object(expected_runtime.get("docker_plugins"), "frozen Docker plugins")
234
+ config = private / "docker"
235
+ directory = config / "cli-plugins"
236
+ directory.mkdir(parents=True, mode=0o700)
237
+ cli = _object(expected_runtime.get("docker_cli"), "frozen Docker CLI")
238
+ actual_cli = _frozen_executable(cli, "CLI")
239
+ binaries = private / "docker-bin"
240
+ binaries.mkdir(mode=0o700)
241
+ (binaries / "docker").symlink_to(actual_cli["resolved_path"])
242
+ env["PATH"] = str(binaries) + os.pathsep + env.get("PATH", os.defpath)
243
+ for name in ("compose", "buildx"):
244
+ plugin = _object(plugins.get(name), f"frozen {name} plugin")
245
+ actual = _frozen_executable(plugin, name + " plugin")
246
+ (directory / ("docker-" + name)).symlink_to(actual["resolved_path"])
247
+ env["DOCKER_CONFIG"] = str(config)
248
+ try:
249
+ selected_cli = shutil.which("docker", path=env["PATH"])
250
+ if selected_cli is None:
251
+ raise ValueError("Private Docker executable is unavailable")
252
+ observed_cli = _executable_identity(Path(selected_cli))
253
+ discovery = run_command(
254
+ ["docker", "info", "--format", "{{json .ClientInfo}}"],
255
+ cwd=private,
256
+ env=env,
257
+ timeout=_DOCKER_TIMEOUT,
258
+ )
259
+ if discovery.returncode:
260
+ raise ValueError("Private Docker client discovery failed")
261
+ client = _object(json.loads(discovery.stdout), "isolated Docker client")
262
+ observed = _docker_plugins({"ClientInfo": client})
263
+ except (OSError, ValueError, subprocess.TimeoutExpired) as error:
264
+ raise isolation.RuntimeDriftError(
265
+ "Cannot verify isolated Docker plugin discovery"
266
+ ) from error
267
+ if (
268
+ client.get("Version") != cli["version"]
269
+ or any(observed_cli[key] != cli[key] for key in ("resolved_path", "sha256"))
270
+ or any(
271
+ observed[name][key] != plugins[name][key]
272
+ for name in ("compose", "buildx")
273
+ for key in ("resolved_path", "sha256", "version")
274
+ )
275
+ ):
276
+ raise isolation.RuntimeDriftError(
277
+ "Isolated Docker executable identity differs from preparation"
278
+ )
279
+ for name in ("compose", "buildx"):
280
+ argv = ["docker", name, "version"] + (["--short"] if name == "compose" else [])
281
+ try:
282
+ result = run_command(argv, cwd=private, env=env, timeout=_DOCKER_TIMEOUT)
283
+ except (OSError, subprocess.TimeoutExpired) as error:
284
+ raise isolation.RuntimeDriftError(f"Cannot verify isolated Docker {name}") from error
285
+ version = plugins[name]["version"]
286
+ if result.returncode or version.removeprefix("v") not in {
287
+ token.removeprefix("v") for token in result.stdout.split()
288
+ }:
289
+ raise isolation.RuntimeDriftError(
290
+ f"Isolated Docker {name} differs from its frozen version"
291
+ )
292
+ return {
293
+ "docker_cli": {**observed_cli, "version": client["Version"]},
294
+ "docker_plugins": observed,
295
+ }
296
+
297
+
298
+ def runtime_identity(*, cpus: int = 1, memory_mb: int = 2048) -> dict[str, object]:
299
+ """Fingerprint the framework, wrapper, selected daemon and enforced limits."""
300
+ try:
301
+ version = importlib.metadata.version("harbor")
302
+ except importlib.metadata.PackageNotFoundError as error:
303
+ raise ValueError(f"Harbor {HARBOR_VERSION} is required for new coding runs") from error
304
+ if version != HARBOR_VERSION:
305
+ raise ValueError(f"Harbor {HARBOR_VERSION} is required; installed version is {version}")
306
+ if cpus < 1 or memory_mb < 128:
307
+ raise ValueError("Container limits require at least one CPU and 128 MB memory")
308
+ info, endpoint = _engine()
309
+ required = ("ID", "ServerVersion", "OSType", "Architecture")
310
+ if not all(isinstance(info.get(key), str) and info[key] for key in required):
311
+ raise ValueError("Docker did not report a complete daemon identity")
312
+ if info["OSType"] != "linux":
313
+ raise ValueError("Harbor execution currently requires Linux containers")
314
+ capacity = {"cpus": info.get("NCPU"), "memory_bytes": info.get("MemTotal")}
315
+ if not all(isinstance(value, int) and value > 0 for value in capacity.values()):
316
+ raise ValueError("Docker did not report usable CPU and memory capacity")
317
+ return {
318
+ "protocol": PROTOCOL,
319
+ "harbor_version": version,
320
+ "submission_receipt_version": 2,
321
+ "codex_transport": {
322
+ "version": _CODEX_TRANSPORT_VERSION,
323
+ "harbor_run_sha256": _assert_codex_transport_source(),
324
+ },
325
+ "launcher_version": components.VERSIONS["launcher"],
326
+ "implementation_sha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
327
+ "docker_cli": _docker_cli(info),
328
+ "docker_plugins": _docker_plugins(info),
329
+ "docker_engine": {
330
+ "id": info["ID"],
331
+ "version": info["ServerVersion"],
332
+ "platform": info["OSType"] + "/" + info["Architecture"],
333
+ "endpoint": endpoint,
334
+ "capacity": capacity,
335
+ },
336
+ "resources": {
337
+ "cpus": cpus,
338
+ "memory_mb": memory_mb,
339
+ "cpu_enforcement": "limit",
340
+ "memory_enforcement": "limit",
341
+ },
342
+ }
343
+
344
+
345
+ # Saved for provenance and never compared: routine Docker and PluginBench updates change them.
346
+ _RECORDED_RUNTIME = (
347
+ ("implementation_sha256",),
348
+ ("docker_cli",),
349
+ ("docker_plugins",),
350
+ ("docker_engine", "id"),
351
+ ("docker_engine", "version"),
352
+ ("docker_engine", "endpoint"),
353
+ )
354
+ # Checked as a fit for the run's containers, never compared: Docker reports memory that jitters.
355
+ _FIT_RUNTIME = (("docker_engine", "capacity"),)
356
+
357
+
358
+ def runtime_contract(identity: dict[str, object], *, launcher: bool = True) -> dict[str, Any]:
359
+ """The part of a runtime identity that must stay equal: all but recorded and fit fields.
360
+
361
+ Any field not listed above is compared, so a field added later is exact by default. A
362
+ runtime saved before declared versions gets its launcher version from its source hash,
363
+ and an unknown hash raises. `launcher=False` leaves the launcher version out, for work
364
+ that starts no coding agent.
365
+ """
366
+ if not isinstance(identity, dict):
367
+ raise ValueError("a saved runtime identity is not an object")
368
+ contract = copy.deepcopy(identity)
369
+ version = contract.get("launcher_version")
370
+ if version is None:
371
+ version = components.saved_version(
372
+ "launcher", {"harbor_backend.py": contract.get("implementation_sha256")}
373
+ )
374
+ if launcher:
375
+ contract["launcher_version"] = version
376
+ else:
377
+ contract.pop("launcher_version", None)
378
+ for *parents, field in _RECORDED_RUNTIME + _FIT_RUNTIME:
379
+ node: object = contract
380
+ for key in parents:
381
+ node = node.get(key) if isinstance(node, dict) else None
382
+ if isinstance(node, dict):
383
+ node.pop(field, None)
384
+ return contract
385
+
386
+
387
+ def runtime_changes(saved: dict[str, Any], current: dict[str, Any], prefix: str = "") -> list[str]:
388
+ """Each differing field of two runtime contracts, by dotted path, with both values."""
389
+ changes = []
390
+ for key in sorted(saved.keys() | current.keys()):
391
+ before, after = saved.get(key, _ABSENT), current.get(key, _ABSENT)
392
+ if isinstance(before, dict) and isinstance(after, dict):
393
+ changes.extend(runtime_changes(before, after, prefix + key + "."))
394
+ elif before != after:
395
+ changes.append(f"{prefix}{key} was {_shown(before)}, is now {_shown(after)}")
396
+ return changes
397
+
398
+
399
+ _ABSENT = object()
400
+
401
+
402
+ def _shown(value: object) -> str:
403
+ return "absent" if value is _ABSENT else json.dumps(value, sort_keys=True)
404
+
405
+
406
+ def _fitting_jobs(capacity: dict[str, Any], *, cpus: int, memory_mb: int) -> int:
407
+ """How many containers of these limits Docker can hold, keeping 20% of memory free."""
408
+ return min(
409
+ int(capacity["cpus"]) // cpus,
410
+ int(int(capacity["memory_bytes"]) * 0.8) // (memory_mb * 1024**2),
411
+ )
412
+
413
+
414
+ def assert_runtime(
415
+ expected: dict[str, object], *, jobs: int = 1, launcher: bool = True
416
+ ) -> dict[str, object]:
417
+ """Check the current runtime against a saved one, and return the current one.
418
+
419
+ Exact fields must be equal, Docker capacity must hold `jobs` containers, and recorded
420
+ fields may differ. Docker commands after this check use the returned identity, so an
421
+ updated Docker CLI or a restarted engine runs with what is installed now.
422
+ """
423
+ resources = _object(expected.get("resources"), "container resources")
424
+ cpus, memory = resources.get("cpus"), resources.get("memory_mb")
425
+ if not isinstance(cpus, int) or not isinstance(memory, int):
426
+ raise ValueError("Frozen container resource limits are missing")
427
+ saved = runtime_contract(expected, launcher=launcher)
428
+ try:
429
+ actual = runtime_identity(cpus=cpus, memory_mb=memory)
430
+ except (OSError, ValueError, subprocess.TimeoutExpired) as error:
431
+ raise isolation.RuntimeDriftError(
432
+ "Cannot verify the frozen Harbor and Docker runtime"
433
+ ) from error
434
+ changes = runtime_changes(saved, runtime_contract(actual, launcher=launcher))
435
+ if changes:
436
+ raise isolation.RuntimeDriftError(
437
+ "Harbor or Docker runtime changed since this run was prepared: "
438
+ + "; ".join(changes)
439
+ + ". Restore that runtime, or start a new run"
440
+ )
441
+ current = _object(_object(actual["docker_engine"], "Docker engine")["capacity"], "capacity")
442
+ if _fitting_jobs(current, cpus=cpus, memory_mb=memory) < jobs:
443
+ engine = expected.get("docker_engine")
444
+ before = engine.get("capacity") if isinstance(engine, dict) else None
445
+ raise isolation.RuntimeDriftError(
446
+ f"Docker capacity no longer fits this run: {jobs} jobs of {cpus} CPUs and "
447
+ f"{memory} MB need {jobs * cpus} CPUs and {jobs * memory} MB within 80% of "
448
+ f"memory; docker_engine.capacity was {_shown(before)}, is now {_shown(current)}. "
449
+ "Give Docker that capacity again, or start a new run"
450
+ )
451
+ return actual
452
+
453
+
454
+ def assert_capacity(inputs: list[list[dict[str, object]]], *, jobs: int) -> None:
455
+ """Check that Docker holds the containers that run at once: `jobs` of `inputs`.
456
+
457
+ Each input runs its containers one after another, so it needs its largest CPU and its
458
+ largest memory limit. The largest needs of the inputs that run at once are summed for
459
+ each resource separately, so different limits are never under-counted. Measurement uses
460
+ this; it starts no coding agent.
461
+ """
462
+ needs = []
463
+ for runtimes in inputs:
464
+ limits = []
465
+ for runtime in runtimes:
466
+ resources = _object(runtime.get("resources"), "container resources")
467
+ cpus, memory = resources.get("cpus"), resources.get("memory_mb")
468
+ if not isinstance(cpus, int) or not isinstance(memory, int):
469
+ raise ValueError("Frozen container resource limits are missing")
470
+ limits.append((cpus, memory))
471
+ needs.append((max(limit[0] for limit in limits), max(limit[1] for limit in limits)))
472
+ count = min(jobs, len(needs))
473
+ cpus = sum(sorted((need[0] for need in needs), reverse=True)[:count])
474
+ memory = sum(sorted((need[1] for need in needs), reverse=True)[:count])
475
+ actual = assert_runtime(inputs[0][0], launcher=False)
476
+ current = _object(_object(actual["docker_engine"], "Docker engine")["capacity"], "capacity")
477
+ if cpus > int(current["cpus"]) or memory * 1024**2 > int(current["memory_bytes"]) * 0.8:
478
+ raise isolation.RuntimeDriftError(
479
+ f"Docker capacity no longer fits this run's measurement: {count} containers at once "
480
+ f"need {cpus} CPUs and {memory} MB within 80% of memory; docker_engine.capacity is "
481
+ f"now {_shown(current)}. Give Docker that capacity again, then resume the run"
482
+ )
483
+
484
+
485
+ def capacity_jobs(*, cpus: int = 1, memory_mb: int = 2048) -> int:
486
+ """Bound coding concurrency by the actual Docker allocation, leaving headroom."""
487
+ if cpus < 1 or memory_mb < 128:
488
+ raise ValueError("Container limits require at least one CPU and 128 MB memory")
489
+ info, _ = _engine()
490
+ cpu_capacity, memory_capacity = info.get("NCPU"), info.get("MemTotal")
491
+ if not isinstance(cpu_capacity, int) or not isinstance(memory_capacity, int):
492
+ raise ValueError("Docker did not report CPU and memory capacity")
493
+ jobs = min(
494
+ 4,
495
+ _fitting_jobs(
496
+ {"cpus": cpu_capacity, "memory_bytes": memory_capacity}, cpus=cpus, memory_mb=memory_mb
497
+ ),
498
+ )
499
+ if jobs < 1:
500
+ raise ValueError(
501
+ f"One coding container ({cpus} CPUs, {memory_mb} MB) does not fit within Docker capacity ({cpu_capacity} CPUs, {memory_capacity // 1024**2} MB; 80% memory budget)"
502
+ )
503
+ return jobs
504
+
505
+
506
+ def preflight(*, jobs: int = 1, cpus: int = 1, memory_mb: int = 2048) -> dict[str, Any]:
507
+ """Check local capabilities and capacity without installing or calling a model."""
508
+ runtime = runtime_identity(cpus=cpus, memory_mb=memory_mb)
509
+ info, _ = _engine()
510
+ if jobs < 1 or jobs * cpus > info.get("NCPU", 0):
511
+ raise ValueError("Requested concurrent container CPU limits exceed Docker capacity")
512
+ if jobs * memory_mb * 1024**2 > info.get("MemTotal", 0) * 0.8:
513
+ raise ValueError("Requested concurrent container memory exceeds 80% of Docker capacity")
514
+ compose = _docker("compose", "version", "--short").strip()
515
+ return {"runtime": runtime, "compose_version": compose, "paid_calls": 0}
516
+
517
+
518
+ def _inspect_image(
519
+ image: str, *, pull: bool, expected_runtime: dict[str, object] | None = None
520
+ ) -> dict[str, Any]:
521
+ try:
522
+ inspected = _docker("image", "inspect", image, expected_runtime=expected_runtime)
523
+ except ValueError as error:
524
+ if not pull or "no such image" not in str(error).lower():
525
+ raise
526
+ _docker("pull", image, timeout=600, expected_runtime=expected_runtime)
527
+ inspected = _docker("image", "inspect", image, expected_runtime=expected_runtime)
528
+ images = json.loads(inspected)
529
+ if not isinstance(images, list) or len(images) != 1:
530
+ raise ValueError("Docker did not identify one image")
531
+ return _object(images[0], "Docker image")
532
+
533
+
534
+ def image_identity(
535
+ image: str, *, pull: bool = False, expected_runtime: dict[str, object] | None = None
536
+ ) -> dict[str, str]:
537
+ """Bind an immutable base image to the local platform that will run it."""
538
+ if re.fullmatch(r"[^\s]+@sha256:[0-9a-f]{64}", image) is None:
539
+ raise ValueError("Agent images must use an immutable sha256 registry digest")
540
+ image_info = _inspect_image(image, pull=pull, expected_runtime=expected_runtime)
541
+ config = _object(image_info.get("Config"), "Docker image config")
542
+ workdir = config.get("WorkingDir")
543
+ if not isinstance(workdir, str) or re.fullmatch(r"/[A-Za-z0-9_-]+", workdir) is None:
544
+ raise ValueError("Base image must have one explicit project directory below /")
545
+ if workdir in {"/root", "/home", "/tmp", "/usr", "/bin", "/etc", "/var", "/logs"}:
546
+ raise ValueError(f"Unsupported base-image project directory: {workdir}")
547
+ return {
548
+ "image": image,
549
+ "image_id": str(image_info["Id"]),
550
+ "platform": str(image_info["Os"]) + "/" + str(image_info["Architecture"]),
551
+ "workdir": workdir,
552
+ }
553
+
554
+
555
+ def resolve_image(image: str) -> dict[str, str]:
556
+ """Resolve a task's image tag once during preparation, then freeze its digest."""
557
+ if not image or any(character.isspace() for character in image):
558
+ raise ValueError("Task image must be a nonempty Docker image reference")
559
+ if "@" in image:
560
+ return {**image_identity(image, pull=True), "original_image": image}
561
+ original = _inspect_image(image, pull=True)
562
+ repository = image.rsplit(":", 1)[0] if ":" in image.rsplit("/", 1)[-1] else image
563
+ digests = original.get("RepoDigests")
564
+ if not isinstance(digests, list):
565
+ raise ValueError("Task image has no immutable repository digest")
566
+ matches = sorted(
567
+ digest
568
+ for digest in digests
569
+ if isinstance(digest, str) and digest.startswith(repository + "@sha256:")
570
+ )
571
+ if not matches:
572
+ raise ValueError("Task image has no immutable digest for the requested repository")
573
+ resolved = image_identity(matches[0])
574
+ if resolved["image_id"] != original.get("Id"):
575
+ raise ValueError("Task image changed while resolving its immutable digest")
576
+ return {**resolved, "original_image": image}
577
+
578
+
579
+ def _normalize_version(version: str) -> str:
580
+ return version.removeprefix("codex-cli ").split()[0]
581
+
582
+
583
+ def _required_tools(harness: str) -> str:
584
+ support = "rg" if harness == "codex" else "ps"
585
+ return f'for tool in {_BASE_TOOLS} {support}; do command -v "$tool" >/dev/null; done'
586
+
587
+
588
+ def _cli_guard(harness: str, version: str) -> str:
589
+ if (
590
+ harness not in {"codex", "claude"}
591
+ or re.fullmatch(r"\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?", version) is None
592
+ ):
593
+ raise ValueError("Toolchains require a supported harness and exact CLI version")
594
+ tools = _required_tools(harness) + "; "
595
+ if harness == "codex":
596
+ return tools + f'test "$(codex --version)" = "codex-cli {version}"'
597
+ return tools + f'actual="$(claude --version)"; test "${{actual%% *}}" = "{version}"'
598
+
599
+
600
+ async def run_trial(config: dict[str, Any]) -> dict[str, Any]:
601
+ """Run one native trial, without queue retries or Harbor resume behavior."""
602
+ # Harbor's wheel is untyped. Keep its dynamic API at this single boundary,
603
+ # without making reporting import its execution dependencies.
604
+ config = dict(config)
605
+ boundary = config.pop("pluginbench_boundary", None)
606
+ agent = _object(config.get("agent"), "native agent configuration")
607
+ separate_codex = agent.get("name") == "codex" and config.get("source_trial") is None
608
+ if separate_codex:
609
+ _assert_codex_transport_source()
610
+ trial_config = importlib.import_module("harbor.models.trial.config").TrialConfig
611
+ trial_type = importlib.import_module("harbor.trial.trial").Trial
612
+ trial = await trial_type.create(trial_config.model_validate(config))
613
+ if boundary is not None:
614
+ install_native_hooks(trial, _native_object(boundary, "native boundary"))
615
+ if separate_codex:
616
+ original_exec = trial.agent.exec_as_agent
617
+ treatment_installed = False
618
+
619
+ async def separated_exec(
620
+ environment: object,
621
+ command: str,
622
+ env: dict[str, str] | None = None,
623
+ cwd: str | None = None,
624
+ timeout_sec: int | None = None,
625
+ ) -> object:
626
+ nonlocal treatment_installed
627
+ if "codex exec" in command:
628
+ if not command.startswith(_CODEX_EXEC_PREFIX) or not command.endswith(
629
+ _CODEX_EXEC_SUFFIX
630
+ ):
631
+ raise ValueError(
632
+ "Native Codex invocation differs from the pinned transport contract"
633
+ )
634
+ if boundary is not None and "treatment" in boundary:
635
+ if treatment_installed:
636
+ raise ValueError("native treatment was already used; use a fresh attempt")
637
+ if env is None:
638
+ raise ValueError("native coding command has no private environment")
639
+ await native_treatments.install(
640
+ environment=environment,
641
+ treatment=Treatment.model_validate(boundary["treatment"]),
642
+ env=env,
643
+ cwd=str(boundary["image"]["workdir"]),
644
+ artifacts=Path(str(boundary["area"])) / "artifacts",
645
+ )
646
+ treatment_installed = True
647
+ command = command.removesuffix(_CODEX_EXEC_SUFFIX) + (
648
+ "2>/logs/agent/codex.stderr.log </dev/null | tee /logs/agent/codex.txt"
649
+ )
650
+ result: object = await original_exec(
651
+ environment,
652
+ command,
653
+ env=env,
654
+ cwd=cwd,
655
+ timeout_sec=timeout_sec,
656
+ )
657
+ return result
658
+
659
+ trial.agent.exec_as_agent = separated_exec
660
+ result = await trial.run()
661
+ return {
662
+ "result": result.model_dump(mode="json"),
663
+ "session_id": trial.agent_environment.session_id,
664
+ }
665
+
666
+
667
+ def _project_ids(kind: str, project: str, expected_runtime: dict[str, object]) -> list[str]:
668
+ arguments = [kind, "ls", "--quiet"] if kind != "container" else ["ps", "-aq"]
669
+ output = _docker(
670
+ *arguments,
671
+ "--filter",
672
+ f"label=com.docker.compose.project={project}",
673
+ expected_runtime=expected_runtime,
674
+ )
675
+ return output.split()
676
+
677
+
678
+ def stopped_container_state(value: object) -> dict[str, object]:
679
+ """Retain engine failure evidence independently of native agent completion."""
680
+ state = _object(value, "container state")
681
+ if state.get("Running") is not False or state.get("Status") not in {"exited", "dead"}:
682
+ raise ValueError("Submission container is not stopped; refusing a live snapshot")
683
+ if (
684
+ type(state.get("OOMKilled")) is not bool
685
+ or type(state.get("ExitCode")) is not int
686
+ or state["ExitCode"] < 0
687
+ ):
688
+ raise ValueError("Docker did not report complete stopped-container failure evidence")
689
+ if not all(isinstance(state.get(key), str) for key in ("Error", "StartedAt", "FinishedAt")):
690
+ raise ValueError("Docker did not report stopped-container error and timestamps")
691
+ return {
692
+ key: state[key]
693
+ for key in (
694
+ "Running",
695
+ "Status",
696
+ "OOMKilled",
697
+ "ExitCode",
698
+ "Error",
699
+ "StartedAt",
700
+ "FinishedAt",
701
+ )
702
+ }
703
+
704
+
705
+ def cleanup(*, project: str, owner: str, expected_runtime: dict[str, object]) -> dict[str, object]:
706
+ """Remove only this trial's labelled resources and verify that none remain."""
707
+ _assert_daemon(expected_runtime)
708
+ containers = _project_ids("container", project, expected_runtime)
709
+ if containers:
710
+ _docker("rm", "--force", "--volumes", *containers, expected_runtime=expected_runtime)
711
+ networks = _project_ids("network", project, expected_runtime)
712
+ if networks:
713
+ _docker("network", "rm", *networks, expected_runtime=expected_runtime)
714
+ images = _docker(
715
+ "image",
716
+ "ls",
717
+ "-q",
718
+ "--filter",
719
+ f"label={_OWNER_LABEL}={owner}",
720
+ expected_runtime=expected_runtime,
721
+ ).split()
722
+ if images:
723
+ _docker("image", "rm", *images, expected_runtime=expected_runtime)
724
+ residuals = {
725
+ "containers": _project_ids("container", project, expected_runtime),
726
+ "networks": _project_ids("network", project, expected_runtime),
727
+ "images": _docker(
728
+ "image",
729
+ "ls",
730
+ "-q",
731
+ "--filter",
732
+ f"label={_OWNER_LABEL}={owner}",
733
+ expected_runtime=expected_runtime,
734
+ ).split(),
735
+ }
736
+ if any(residuals.values()):
737
+ raise ValueError(f"Owned Harbor resources remain after cleanup: {residuals}")
738
+ _assert_daemon(expected_runtime)
739
+ return {"status": "complete", "residuals": residuals}
740
+
741
+
742
+ def _native_evidence(
743
+ *,
744
+ area: Path,
745
+ host: str,
746
+ effort: str,
747
+ transport: str | None = None,
748
+ include_descendants: bool = False,
749
+ ) -> dict[str, Any]:
750
+ codex.validate_artifacts(area / "artifacts")
751
+ agent = area / "artifacts/harbor/trial/agent"
752
+ raw = agent / ("codex.txt" if host == "codex" else "claude-code.txt")
753
+
754
+ stdout = raw.read_text(errors="replace") if raw.is_file() else ""
755
+ sessions = agent / "sessions"
756
+ traces = [path.read_text(errors="replace") for path in sorted(sessions.rglob("*.jsonl"))]
757
+ artifacts = area / "artifacts"
758
+ if host == "codex":
759
+ if transport != _CODEX_TRANSPORT_VERSION:
760
+ raise ValueError("Unsupported frozen Codex transport version")
761
+ stderr = agent / "codex.stderr.log"
762
+ if not raw.is_file() or not stderr.is_file():
763
+ raise ValueError("Native Codex separate stdout/stderr capture is incomplete")
764
+ shutil.copyfile(stderr, artifacts / "codex.stderr.log")
765
+ (artifacts / "stdout.jsonl").write_text(stdout)
766
+ for index, trace in enumerate(traces):
767
+ (artifacts / f"host-trace-{index}.jsonl").write_text(trace)
768
+ return codex.parse_events(
769
+ stdout, traces, host, requested_effort=effort, include_descendants=include_descendants
770
+ )
771
+
772
+
773
+ def _native_object(value: object, label: str) -> dict[str, object]:
774
+ if not isinstance(value, dict) or not all(isinstance(key, str) for key in value):
775
+ raise ValueError(f"{label} must be an object")
776
+ return {key: item for key, item in value.items()}
777
+
778
+
779
+ def _portable_mode(path: Path) -> int:
780
+ """Git tracks only the executable bit, so group and other bits depend on the checkout's umask."""
781
+ return 0o755 if path.stat().st_mode & 0o100 else 0o644
782
+
783
+
784
+ def _native_asset_inventory(task_dir: Path, *, exclude: str | None = None) -> dict[str, str]:
785
+ files: dict[str, str] = {}
786
+ if task_dir.is_symlink() or not task_dir.is_dir():
787
+ raise ValueError("Native task must be a directory, not a link")
788
+ for path in sorted(task_dir.rglob("*")):
789
+ if path.is_symlink() or not (path.is_dir() or path.is_file()):
790
+ raise ValueError(f"Unsupported native task asset: {path}")
791
+ if path.relative_to(task_dir).as_posix() == exclude:
792
+ continue
793
+ if path.is_file():
794
+ files[path.relative_to(task_dir).as_posix()] = hashlib.sha256(
795
+ str(_portable_mode(path)).encode() + b"\0" + path.read_bytes()
796
+ ).hexdigest()
797
+ else:
798
+ files[path.relative_to(task_dir).as_posix() + "/"] = "directory:" + str(0o755)
799
+ return files
800
+
801
+
802
+ def native_task_assets(task_dir: Path) -> str:
803
+ """Bind all asset bytes, directories and modes without following links."""
804
+ return hashlib.sha256(
805
+ json.dumps(_native_asset_inventory(task_dir), sort_keys=True).encode()
806
+ ).hexdigest()
807
+
808
+
809
+ def validate_native_task(task_dir: Path) -> dict[str, object]:
810
+ """Admit standard tasks by execution topology, never by task name."""
811
+ native_task_assets(task_dir)
812
+ task = importlib.import_module("harbor.models.task.task").Task(task_dir)
813
+ modes = importlib.import_module("harbor.models.task.verifier_mode")
814
+ if task.has_steps or modes.resolve_task_verifier_mode(task.config).value != "separate":
815
+ raise ValueError("Native execution requires a single-step task with a separate verifier")
816
+ definition = modes.resolve_verifier_environment_definition(task.config, task.paths)
817
+ if definition is None:
818
+ raise ValueError("Native separate verifier definition is missing")
819
+ for directory, configuration in (
820
+ (task.paths.environment_dir, task.config.environment),
821
+ (definition.directory, definition.config),
822
+ ):
823
+ if configuration.os.value != "linux" or configuration.gpus or configuration.tpu:
824
+ raise ValueError("Native execution supports Linux CPU tasks only")
825
+ if any(
826
+ key in configuration.env
827
+ for key in ("OPENAI_BASE_URL", "ANTHROPIC_BASE_URL", "CODEX_HOME", "CLAUDE_CONFIG_DIR")
828
+ ):
829
+ raise ValueError(
830
+ "Custom native provider endpoints or agent configuration directories are unsupported"
831
+ )
832
+ if (directory / "docker-compose.yaml").exists() or (
833
+ directory / "docker-compose.yml"
834
+ ).exists():
835
+ raise ValueError(
836
+ "Task-authored Docker Compose is unsupported: host mounts and service topology are not admitted"
837
+ )
838
+ if not configuration.docker_image and not (directory / "Dockerfile").is_file():
839
+ raise ValueError("Native environment needs a Docker image or Dockerfile")
840
+ image = task.config.environment.docker_image
841
+ dockerfile = task.paths.environment_dir / "Dockerfile"
842
+ return {
843
+ "task_assets_sha256": native_task_assets(task_dir),
844
+ # Agents run in a prebuilt image as published; a Dockerfile beside it is never built.
845
+ "coding_image": {
846
+ "source": "prebuilt" if image else "dockerfile",
847
+ "image": image,
848
+ "ignored_dockerfile": str(dockerfile.relative_to(task_dir))
849
+ if image and dockerfile.is_file()
850
+ else None,
851
+ },
852
+ "configuration": task.config.model_dump(mode="json"),
853
+ "verifier_directory": str(definition.directory),
854
+ "verifier_bundled_tests": definition.bundled_tests,
855
+ "verifier_configuration": definition.config.model_dump(mode="json"),
856
+ }
857
+
858
+
859
+ def _native_image(
860
+ *, directory: Path, image: str | None, expected: dict[str, object]
861
+ ) -> dict[str, str]:
862
+ if image:
863
+ description = _inspect_image(image, pull=True, expected_runtime=expected)
864
+ else:
865
+ description = _build_native_image(directory=directory, expected=expected)
866
+ image_id = description.get("Id")
867
+ if not isinstance(image_id, str) or re.fullmatch(r"sha256:[0-9a-f]{64}", image_id) is None:
868
+ raise ValueError("Native environment did not resolve to an immutable image ID")
869
+ config = _native_object(description.get("Config"), "native image config")
870
+ return {
871
+ "image": _native_image_reference(image_id=image_id, expected=expected),
872
+ "image_id": image_id,
873
+ "platform": str(description["Os"]) + "/" + str(description["Architecture"]),
874
+ "workdir": str(config.get("WorkingDir") or "/"),
875
+ }
876
+
877
+
878
+ def _build_native_image(*, directory: Path, expected: dict[str, object]) -> dict[str, object]:
879
+ tag = "pluginbench-native-build:" + uuid4().hex
880
+ try:
881
+ _inspect_image(tag, pull=False, expected_runtime=expected)
882
+ except ValueError as error:
883
+ if "no such image" not in str(error).lower():
884
+ raise
885
+ else:
886
+ raise ValueError("Native temporary build reference already exists")
887
+ image_id: str | None = None
888
+ failure: BaseException | None = None
889
+ try:
890
+ # Tagged export loads an inspectable image; raw quiet output may be a config digest.
891
+ _docker(
892
+ "build",
893
+ "--quiet",
894
+ "--provenance=false",
895
+ "--tag",
896
+ tag,
897
+ str(directory),
898
+ timeout=1800,
899
+ expected_runtime=expected,
900
+ )
901
+ description = _native_object(
902
+ _inspect_image(tag, pull=False, expected_runtime=expected), "built native image"
903
+ )
904
+ candidate = description.get("Id")
905
+ if (
906
+ not isinstance(candidate, str)
907
+ or re.fullmatch(r"sha256:[0-9a-f]{64}", candidate) is None
908
+ ):
909
+ raise ValueError("Native build did not expose its immutable image ID")
910
+ image_id = candidate
911
+ _native_image_reference(image_id=image_id, expected=expected)
912
+ return description
913
+ except BaseException as error:
914
+ failure = error
915
+ raise
916
+ finally:
917
+ try:
918
+ remaining = _inspect_image(tag, pull=False, expected_runtime=expected)
919
+ except ValueError as error:
920
+ if "no such image" not in str(error).lower():
921
+ if failure is None:
922
+ raise
923
+ failure.add_note("Native build cleanup inspection failed: " + str(error))
924
+ else:
925
+ try:
926
+ if image_id is None or remaining.get("Id") != image_id:
927
+ raise ValueError(
928
+ "Cannot prove temporary native build ownership; reference retained: " + tag
929
+ )
930
+ _native_image_reference(image_id=image_id, expected=expected)
931
+ _docker("image", "rm", "--no-prune", tag, expected_runtime=expected)
932
+ except (OSError, ValueError, subprocess.TimeoutExpired) as error:
933
+ if failure is None:
934
+ raise
935
+ failure.add_note("Native build cleanup incomplete: " + str(error))
936
+
937
+
938
+ def _native_image_reference(*, image_id: str, expected: dict[str, object]) -> str:
939
+ """BuildKit needs a named local reference; verify its immutable target."""
940
+ if re.fullmatch(r"sha256:[0-9a-f]{64}", image_id) is None:
941
+ raise ValueError("Native image must have a complete immutable ID")
942
+ reference = "pluginbench-native:sha256-" + image_id.removeprefix("sha256:")
943
+ try:
944
+ image = _inspect_image(reference, pull=False, expected_runtime=expected)
945
+ except ValueError as error:
946
+ if "no such image" not in str(error).lower():
947
+ raise
948
+ _docker("tag", image_id, reference, expected_runtime=expected)
949
+ image = _inspect_image(reference, pull=False, expected_runtime=expected)
950
+ if image.get("Id") != image_id:
951
+ raise ValueError("Native image reference was rebound to different content")
952
+ return reference
953
+
954
+
955
+ def _bundle_native_tests(
956
+ *, tests: Path, grader: dict[str, str], expected: dict[str, object]
957
+ ) -> dict[str, str]:
958
+ """Move the native verifier's exact uploaded tests into its frozen image."""
959
+ with tempfile.TemporaryDirectory(prefix="pluginbench-native-tests-") as temporary:
960
+ context = Path(temporary)
961
+ shutil.copytree(tests, context / "tests")
962
+ (context / "Dockerfile").write_text("FROM " + grader["image"] + "\nCOPY tests/ /tests/\n")
963
+ return _native_image(directory=context, image=None, expected=expected)
964
+
965
+
966
+ def _assert_native_image(identity: dict[str, object], expected: dict[str, object]) -> None:
967
+ for reference in (identity["image_id"], identity["image"]):
968
+ if (
969
+ _inspect_image(str(reference), pull=False, expected_runtime=expected).get("Id")
970
+ != identity["image_id"]
971
+ ):
972
+ raise ValueError("Frozen native image is unavailable or its reference was rebound")
973
+
974
+
975
+ def engine_id(runtime: object) -> object:
976
+ engine = runtime.get("docker_engine") if isinstance(runtime, dict) else None
977
+ return engine.get("id") if isinstance(engine, dict) else None
978
+
979
+
980
+ def assert_prepared_images(
981
+ identity: dict[str, object], runtime: dict[str, object], *, label: str
982
+ ) -> None:
983
+ """Check that the current engine holds a preparation's images; name both engines if not."""
984
+ for name in ("image_identity", "grader_image_identity"):
985
+ image = _native_object(identity[name], "prepared native image")
986
+ try:
987
+ _assert_native_image(image, runtime)
988
+ except ValueError as error:
989
+ raise ValueError(
990
+ f"{label} was prepared on Docker engine {engine_id(identity.get('isolation'))}; "
991
+ f"the current engine {engine_id(runtime)} does not hold its image "
992
+ f"{image['image_id']}. Restore that engine's images, or start a new run"
993
+ ) from error
994
+
995
+
996
+ def _native_container(
997
+ *, project: str, trial_dir: Path, expected: dict[str, object], image_id: str | None = None
998
+ ) -> dict[str, object]:
999
+ _assert_daemon(expected)
1000
+ ids = _project_ids("container", project, expected)
1001
+ descriptions = json.loads(_docker("inspect", *ids, expected_runtime=expected)) if ids else []
1002
+ mains = [
1003
+ item
1004
+ for item in descriptions
1005
+ if _object(_object(item, "native container").get("Config"), "native config")
1006
+ .get("Labels", {})
1007
+ .get("com.docker.compose.service")
1008
+ == "main"
1009
+ ]
1010
+ if len(mains) != 1:
1011
+ raise ValueError("Native project must have exactly one main container")
1012
+ container = _native_object(mains[0], "native main")
1013
+ config = _native_object(container.get("Config"), "native container config")
1014
+ labels = _native_object(config.get("Labels"), "native container labels")
1015
+ if labels.get("com.docker.compose.project") != project or not re.fullmatch(
1016
+ r"[0-9a-f]{64}", str(container.get("Id"))
1017
+ ):
1018
+ raise ValueError("Native container ownership does not match the trial identity")
1019
+ if image_id is not None and container.get("Image") != image_id:
1020
+ raise ValueError("Native container differs from the frozen image")
1021
+ actual = _native_object(container.get("HostConfig"), "native resources")
1022
+ limits = _native_object(expected.get("resources"), "native limits")
1023
+ if (
1024
+ actual.get("NanoCpus") != int(str(limits["cpus"])) * 10**9
1025
+ or actual.get("Memory") != int(str(limits["memory_mb"])) * 1024**2
1026
+ ):
1027
+ raise ValueError("Native container resource limits differ from the frozen settings")
1028
+ if (
1029
+ actual.get("Privileged")
1030
+ or actual.get("CapAdd")
1031
+ or actual.get("PidMode") == "host"
1032
+ or actual.get("NetworkMode") == "host"
1033
+ ):
1034
+ raise ValueError("Native container has unsupported privileged or host access")
1035
+ allowed = {
1036
+ "/logs/agent": trial_dir / "agent",
1037
+ "/logs/verifier": trial_dir / "verifier",
1038
+ "/logs/artifacts": trial_dir / "artifacts/logs/artifacts",
1039
+ }
1040
+ mounts = container.get("Mounts")
1041
+ if not isinstance(mounts, list):
1042
+ raise ValueError("Native container did not report its mounts")
1043
+ for item in mounts:
1044
+ mount = _native_object(item, "native mount")
1045
+ target = mount.get("Destination")
1046
+ if (
1047
+ not isinstance(target, str)
1048
+ or target not in allowed
1049
+ or mount.get("Type") != "bind"
1050
+ or Path(str(mount.get("Source"))).resolve() != allowed[target].resolve()
1051
+ ):
1052
+ raise ValueError("Native container has an unapproved host mount")
1053
+ return container
1054
+
1055
+
1056
+ def export_native_source(
1057
+ *,
1058
+ destination: Path,
1059
+ project: str,
1060
+ trial_dir: Path,
1061
+ workdir: str,
1062
+ expected: dict[str, object],
1063
+ image_id: str | None = None,
1064
+ resume: bool = False,
1065
+ fresh_source: bool = False,
1066
+ agent_workspace: bool = False,
1067
+ ) -> dict[str, object]:
1068
+ """Freeze actual stopped source before handing it to another phase.
1069
+
1070
+ A symlink is refused, except in an `agent_workspace`: there it is left out and listed in
1071
+ `excluded_runtime_paths`. What an agent leaves beside its work, such as a virtual
1072
+ environment's `bin/python`, decides nothing, because the graded submission is rebuilt from
1073
+ the task's fixture and the committed patch.
1074
+ """
1075
+ if (
1076
+ re.fullmatch(r"/(?:[A-Za-z0-9_.-]+/?)+", workdir) is None
1077
+ or workdir in {"/root", "/home", "/tmp", "/usr", "/etc", "/var", "/logs"}
1078
+ or ".." in Path(workdir).parts
1079
+ ):
1080
+ raise ValueError("Native image must declare a safe explicit repository workdir")
1081
+ container = _native_container(
1082
+ project=project, trial_dir=trial_dir, expected=expected, image_id=image_id
1083
+ )
1084
+ container_id = str(container["Id"])
1085
+ if resume:
1086
+ _docker("stop", "--time", "10", container_id, expected_runtime=expected)
1087
+ container = _native_container(
1088
+ project=project, trial_dir=trial_dir, expected=expected, image_id=image_id
1089
+ )
1090
+ state = stopped_container_state(container.get("State"))
1091
+ destination.mkdir(parents=True, exist_ok=False)
1092
+ excluded_runtime_paths: list[str] = []
1093
+ with tempfile.TemporaryDirectory(
1094
+ prefix="pluginbench-native-export-", dir=destination.parent
1095
+ ) as temporary:
1096
+ raw = Path(temporary)
1097
+ _docker(
1098
+ "cp", f"{container_id}:{workdir}/.", str(raw), timeout=300, expected_runtime=expected
1099
+ )
1100
+
1101
+ def copy(directory: Path, target: Path) -> None:
1102
+ for path in sorted(directory.iterdir()):
1103
+ if path.name in EXCLUDED_NAMES:
1104
+ continue
1105
+ if path.is_symlink():
1106
+ if not agent_workspace:
1107
+ raise ValueError(
1108
+ f"Native source contains a symlink: {path.relative_to(raw)}"
1109
+ )
1110
+ excluded_runtime_paths.append(path.relative_to(raw).as_posix())
1111
+ continue
1112
+ if fresh_source and path.name == ".pytest_cache" and path.is_dir():
1113
+ excluded_runtime_paths.append(path.relative_to(raw).as_posix())
1114
+ continue
1115
+ output = target / path.name
1116
+ if path.is_dir():
1117
+ output.mkdir()
1118
+ copy(path, output)
1119
+ shutil.copystat(path, output, follow_symlinks=False)
1120
+ elif path.is_file():
1121
+ shutil.copy2(path, output)
1122
+ else:
1123
+ raise ValueError("Native source contains a nonregular file")
1124
+
1125
+ copy(raw, destination)
1126
+ snapshot = inspect_snapshot(destination)
1127
+ receipt = {
1128
+ "status": "complete",
1129
+ "stopped": True,
1130
+ "container_id": container_id,
1131
+ "project": project,
1132
+ "state": state,
1133
+ "snapshot_digest": snapshot["digest"],
1134
+ "source_metadata_digest": native_task_assets(destination),
1135
+ "capture_policy": "fresh-source-v1" if fresh_source else "exact-replay-v1",
1136
+ "excluded_runtime_paths": excluded_runtime_paths,
1137
+ "image_id": container["Image"],
1138
+ "workdir": workdir,
1139
+ "resources": {
1140
+ key: value
1141
+ for key, value in _native_object(
1142
+ expected.get("resources", {}), "native export resources"
1143
+ ).items()
1144
+ if key in {"cpus", "memory_mb"}
1145
+ },
1146
+ }
1147
+ if resume:
1148
+ _docker("start", container_id, expected_runtime=expected)
1149
+ return receipt
1150
+
1151
+
1152
+ def install_native_hooks(trial: object, boundary: dict[str, object]) -> None:
1153
+ """Attach public Harbor hooks without changing native verifier semantics."""
1154
+ events = importlib.import_module("harbor.trial.hooks").TrialEvent
1155
+ area = Path(str(boundary["area"]))
1156
+ expected = _native_object(boundary["runtime"], "native runtime")
1157
+ image = _native_object(boundary["image"], "native image")
1158
+ # Harbor's runtime objects remain at this untyped SDK boundary.
1159
+ trial_name = getattr(getattr(trial, "config"), "trial_name")
1160
+ add_hook = getattr(trial, "add_hook")
1161
+ project = trial_name + "__env"
1162
+ trial_dir = area / "artifacts/harbor" / trial_name
1163
+
1164
+ async def freeze_before(event: object) -> None:
1165
+ receipt = export_native_source(
1166
+ destination=area / "before",
1167
+ project=project,
1168
+ trial_dir=trial_dir,
1169
+ workdir=str(image["workdir"]),
1170
+ expected=expected,
1171
+ image_id=str(image["image_id"]),
1172
+ resume=True,
1173
+ fresh_source=True,
1174
+ )
1175
+ codex.write_json(area / "artifacts/before-source.json", receipt)
1176
+ if receipt["snapshot_digest"] != boundary["source_digest"]:
1177
+ raise ValueError(
1178
+ "Native starting source differs from the prepared fixture; model call refused"
1179
+ )
1180
+ metadata = boundary.get("source_metadata_digest")
1181
+ if metadata is not None and receipt.get("source_metadata_digest") != metadata:
1182
+ raise ValueError("Native starting source metadata changed; model call refused")
1183
+
1184
+ async def freeze_after(event: object) -> None:
1185
+ receipt = export_native_source(
1186
+ destination=area / "workspace",
1187
+ project=project,
1188
+ trial_dir=trial_dir,
1189
+ workdir=str(image["workdir"]),
1190
+ expected=expected,
1191
+ image_id=str(image["image_id"]),
1192
+ fresh_source=True,
1193
+ agent_workspace=True,
1194
+ )
1195
+ codex.write_json(area / "artifacts/submission-source.json", receipt)
1196
+
1197
+ agent = getattr(trial, "agent")
1198
+ if boundary.get("install_only"):
1199
+ original_install = getattr(agent, "install")
1200
+
1201
+ async def qualified_install(environment: object) -> None:
1202
+ await original_install(environment)
1203
+ result = await getattr(agent, "exec_as_agent")(
1204
+ environment,
1205
+ command=_native_cli_qualification(
1206
+ str(boundary["harness"]), str(boundary["version"])
1207
+ ),
1208
+ )
1209
+ if getattr(result, "return_code") != 0:
1210
+ raise ValueError("Native CLI qualification failed during setup")
1211
+ command = (
1212
+ "set -eu; rm -rf /tmp/codex-home /tmp/codex-secrets /root/.codex /root/.claude /root/.agents /root/.config /root/.claude.json /AGENTS.md /AGENTS.override.md /CLAUDE.md; find "
1213
+ + shlex.quote(str(image["workdir"]))
1214
+ )
1215
+ command += (
1216
+ " -mindepth 1 \\( -name .git -o -name node_modules \\) -prune -o \\( "
1217
+ + " -o ".join("-name " + shlex.quote(name) for name in sorted(codex.GUIDANCE_NAMES))
1218
+ + " \\) -prune -exec rm -rf -- {} +"
1219
+ )
1220
+ result = await getattr(agent, "exec_as_root")(environment, command=command)
1221
+ if getattr(result, "return_code") != 0:
1222
+ raise ValueError("Native source sanitation failed during setup")
1223
+
1224
+ setattr(agent, "install", qualified_install)
1225
+ else:
1226
+ add_hook(events.AGENT_START, freeze_before)
1227
+ add_hook(events.VERIFICATION_START, freeze_after)
1228
+
1229
+ async def verify_installed(environment: object) -> None:
1230
+ execute = getattr(agent, "exec_as_agent")
1231
+ result = await execute(
1232
+ environment,
1233
+ _native_cli_qualification(str(boundary["harness"]), str(boundary["version"])),
1234
+ )
1235
+ if getattr(result, "return_code") != 0:
1236
+ raise ValueError("Frozen native CLI changed; model call refused")
1237
+
1238
+ setattr(agent, "install", verify_installed)
1239
+
1240
+
1241
+ def _native_cli_qualification(harness: str, version: str) -> str:
1242
+ prelude = _CODEX_EXEC_PREFIX.partition("codex exec")[0] if harness == "codex" else ""
1243
+ return prelude + "set -eu; " + _cli_guard(harness, version)
1244
+
1245
+
1246
+ def _native_config(
1247
+ *,
1248
+ task_dir: Path,
1249
+ area: Path,
1250
+ owner: str,
1251
+ harness: str,
1252
+ version: str,
1253
+ limits: dict[str, object],
1254
+ timeout: int,
1255
+ model: str | None = None,
1256
+ effort: str | None = None,
1257
+ prompt: str = "",
1258
+ treatment: Treatment | None = None,
1259
+ capabilities: NativeCapabilities | None = None,
1260
+ agent_env: dict[str, str] | None = None,
1261
+ ) -> dict[str, object]:
1262
+ kwargs: dict[str, object] = {
1263
+ "version": version,
1264
+ "config": {"features": {"code_mode": True, "code_mode_only": True, "multi_agent": False}}
1265
+ if harness == "codex"
1266
+ else {},
1267
+ }
1268
+ if effort is not None:
1269
+ kwargs["reasoning_effort"] = effort
1270
+ if treatment is not None:
1271
+ native_treatments.admit(treatment, harness)
1272
+ if prompt:
1273
+ raise ValueError("native treatments cannot be appended to the task prompt")
1274
+ if capabilities is None or model is None or effort is None:
1275
+ raise ValueError("native treatment execution requires a fixed capability/model policy")
1276
+ kwargs["config"] = capabilities.codex_config(model=model, effort=effort)
1277
+ kwargs["dangerously_bypass_hook_trust"] = True
1278
+ return {
1279
+ "task": {"path": str(task_dir.resolve())},
1280
+ "trial_name": "pluginbench-" + owner,
1281
+ "trials_dir": str((area / "artifacts/harbor").resolve()),
1282
+ "agent": {
1283
+ "name": "codex" if harness == "codex" else "claude-code",
1284
+ "model_name": model,
1285
+ "override_timeout_sec": timeout,
1286
+ "override_setup_timeout_sec": 600,
1287
+ "kwargs": kwargs,
1288
+ "skills": [str(path) for path in treatment.skills] if treatment is not None else [],
1289
+ "mcp_servers": [],
1290
+ "extra_allowed_hosts": NATIVE_PROVIDER_HOSTS[harness],
1291
+ # Set only when PluginBench supplies a git identity, so other launches are unchanged.
1292
+ **({"env": agent_env} if agent_env else {}),
1293
+ },
1294
+ "environment": {
1295
+ "type": "docker",
1296
+ "delete": True,
1297
+ "override_cpus": limits["cpus"],
1298
+ "override_memory_mb": limits["memory_mb"],
1299
+ "cpu_enforcement_policy": "limit",
1300
+ "memory_enforcement_policy": "limit",
1301
+ "kwargs": {"keep_containers": True},
1302
+ },
1303
+ "verifier": {"disable": False},
1304
+ "extra_instructions": [prompt] if prompt else [],
1305
+ }
1306
+
1307
+
1308
+ def _native_worker_budget(*, task_dir: Path, agent_seconds: int = 0, prepare: bool = False) -> int:
1309
+ task = importlib.import_module("harbor.models.task.task").Task(task_dir)
1310
+ build = int(task.config.environment.build_timeout_sec)
1311
+ if prepare:
1312
+ return build + 600 + 360
1313
+ collect = sum(int(item.timeout_sec or 300) for item in task.config.verifier.collect)
1314
+ return agent_seconds + 600 + 2 * build + int(task.config.verifier.timeout_sec) + collect + 720
1315
+
1316
+
1317
+ def _native_process(
1318
+ *,
1319
+ area: Path,
1320
+ config: dict[str, object],
1321
+ expected: dict[str, object],
1322
+ harness: str,
1323
+ timeout: int,
1324
+ install_only: bool,
1325
+ auth_file: Path | None = None,
1326
+ ) -> dict[str, object]:
1327
+ private = area / "private"
1328
+ artifacts = area / "artifacts"
1329
+ secrets: tuple[str, ...] = ()
1330
+ if install_only:
1331
+ private.mkdir(mode=0o700)
1332
+ home = private / "home"
1333
+ home.mkdir()
1334
+ env = {
1335
+ key: value
1336
+ for key, value in os.environ.items()
1337
+ if key
1338
+ in {
1339
+ "PATH",
1340
+ "LANG",
1341
+ "SSL_CERT_FILE",
1342
+ "SSL_CERT_DIR",
1343
+ "HTTP_PROXY",
1344
+ "HTTPS_PROXY",
1345
+ "NO_PROXY",
1346
+ }
1347
+ }
1348
+ env.update(HOME=str(home), XDG_CONFIG_HOME=str(home / ".config"))
1349
+ else:
1350
+ env, secrets = codex.runtime_environment(private, harness, auth_file=auth_file)
1351
+ if harness == "codex":
1352
+ env["CODEX_AUTH_JSON_PATH"] = str(private / "codex/auth.json")
1353
+ engine = _native_object(expected["docker_engine"], "native engine")
1354
+ env.update(
1355
+ DOCKER_HOST=str(engine["endpoint"]),
1356
+ PYTHON_DOTENV_DISABLED="1",
1357
+ PYTHONPATH=str(Path(__file__).resolve().parents[1]),
1358
+ PYTHONDONTWRITEBYTECODE="1",
1359
+ )
1360
+ try:
1361
+ codex.write_json(
1362
+ artifacts / "docker-client.json",
1363
+ _private_docker_config(private=private, env=env, expected_runtime=expected),
1364
+ )
1365
+ codex.write_json(private / "trial.json", config)
1366
+ result = run_command(
1367
+ [
1368
+ sys.executable,
1369
+ "-m",
1370
+ "pluginbench.harbor_backend",
1371
+ str(private / "trial.json"),
1372
+ str(artifacts / "worker.json"),
1373
+ ],
1374
+ cwd=private,
1375
+ env=env,
1376
+ timeout=timeout,
1377
+ )
1378
+ (artifacts / "harbor-worker.stdout").write_text(result.stdout)
1379
+ (artifacts / "harbor-worker.stderr").write_text(result.stderr)
1380
+ if result.returncode:
1381
+ raise ValueError(f"Native Harbor worker failed (exit {result.returncode})")
1382
+ return _native_object(json.loads((artifacts / "worker.json").read_text()), "native worker")
1383
+ finally:
1384
+ shutil.rmtree(private)
1385
+ codex.scrub_artifacts(artifacts, secrets)
1386
+
1387
+
1388
+ def _qualify_native_source(
1389
+ *,
1390
+ task_dir: Path,
1391
+ fixture: Path,
1392
+ coding_image: dict[str, str],
1393
+ grader: dict[str, str],
1394
+ expected: dict[str, object],
1395
+ destination: Path,
1396
+ ) -> dict[str, object]:
1397
+ """No-model proof of the reviewed grader's base inventory and initial source parity."""
1398
+ proof: dict[str, object] = {"status": "unknown", "source_policy": submissions.SOURCE_POLICY}
1399
+ try:
1400
+ contract = submissions.reviewed_contract(task_dir)
1401
+ except (OSError, ValueError) as error:
1402
+ proof["reason"] = str(error)
1403
+ return proof
1404
+ destination.mkdir(parents=True, exist_ok=False)
1405
+ owner = uuid4().hex
1406
+ project = "pluginbench-source-proof-" + owner
1407
+ limits = _native_object(expected["resources"], "source proof resources")
1408
+ container_id = ""
1409
+ try:
1410
+ base = str(contract["base_commit"])
1411
+ workdir = grader["workdir"]
1412
+ if workdir != "/app" or coding_image["workdir"] != "/app":
1413
+ raise ValueError("reviewed native grader requires the /app workdir")
1414
+ frozen_grader = _inspect_image(grader["image_id"], pull=False, expected_runtime=expected)
1415
+ if frozen_grader.get("Id") != grader["image_id"]:
1416
+ raise ValueError("native grader image identity changed during source qualification")
1417
+ startup = submissions.grader_startup(
1418
+ config=frozen_grader.get("Config"), image_id=grader["image_id"]
1419
+ )
1420
+ command = _base_inventory_command(base)
1421
+ container_id = _docker(
1422
+ "create",
1423
+ "--label",
1424
+ "com.docker.compose.project=" + project,
1425
+ "--label",
1426
+ "com.docker.compose.service=main",
1427
+ "--label",
1428
+ _OWNER_LABEL + "=" + owner,
1429
+ "--cpus",
1430
+ str(limits["cpus"]),
1431
+ "--memory",
1432
+ str(limits["memory_mb"]) + "m",
1433
+ "--network",
1434
+ "none",
1435
+ "--workdir",
1436
+ workdir,
1437
+ "--entrypoint",
1438
+ "bash",
1439
+ grader["image_id"],
1440
+ "-c",
1441
+ command,
1442
+ expected_runtime=expected,
1443
+ ).strip()
1444
+ # Not `start --attach`: that exits with the container's status, and a failed inventory
1445
+ # would then surface as "docker start failed" without the reason in the logs.
1446
+ _docker("start", container_id, expected_runtime=expected)
1447
+ exit_code = _docker("wait", container_id, timeout=60, expected_runtime=expected).strip()
1448
+ output = _docker("logs", container_id, expected_runtime=expected)
1449
+ if exit_code != "0":
1450
+ raise ValueError(
1451
+ f"native grader base inventory command failed (exit {exit_code}): "
1452
+ + output.strip()
1453
+ )
1454
+ (destination / "base-tree.raw").write_text(output)
1455
+ head, separator, inventory = output.partition("\n")
1456
+ if not separator or head != base:
1457
+ raise ValueError("native grader HEAD differs from reviewed verifier base")
1458
+ tree = submissions.base_tree_manifest(inventory)
1459
+ exported = export_native_source(
1460
+ destination=destination / "fixture",
1461
+ project=project,
1462
+ trial_dir=destination,
1463
+ workdir=workdir,
1464
+ expected=expected,
1465
+ image_id=grader["image_id"],
1466
+ fresh_source=True,
1467
+ )
1468
+ if exported["snapshot_digest"] != inspect_snapshot(fixture)["digest"] or exported[
1469
+ "source_metadata_digest"
1470
+ ] != native_task_assets(fixture):
1471
+ raise ValueError("native grader initial source differs from frozen coding fixture")
1472
+ payload_dir = destination / "tests"
1473
+ payload_dir.mkdir()
1474
+ _docker(
1475
+ "cp",
1476
+ container_id + ":/tests/.",
1477
+ str(payload_dir),
1478
+ expected_runtime=expected,
1479
+ timeout=60,
1480
+ )
1481
+ _native_asset_inventory(payload_dir)
1482
+ payload = {
1483
+ path.name: hashlib.sha256(path.read_bytes()).hexdigest()
1484
+ for path in payload_dir.iterdir()
1485
+ if path.is_file()
1486
+ }
1487
+ if (
1488
+ payload != contract["verifier_payload"]
1489
+ or any(not path.is_file() for path in payload_dir.iterdir())
1490
+ or not (payload_dir / "test.sh").stat().st_mode & 0o111
1491
+ ):
1492
+ raise ValueError("native grader image verifier differs from reviewed frozen tests")
1493
+ proof.update(
1494
+ status="complete",
1495
+ contract=contract,
1496
+ grader_head=head,
1497
+ base_tree=tree,
1498
+ base_tree_sha256=hashlib.sha256(json.dumps(tree, sort_keys=True).encode()).hexdigest(),
1499
+ fixture_digest=exported["snapshot_digest"],
1500
+ fixture_metadata_digest=exported["source_metadata_digest"],
1501
+ coding_image_id=coding_image["image_id"],
1502
+ grader_image_id=grader["image_id"],
1503
+ grader_tests_sha256=hashlib.sha256(
1504
+ json.dumps(payload, sort_keys=True).encode()
1505
+ ).hexdigest(),
1506
+ grader_startup=startup,
1507
+ export=exported,
1508
+ )
1509
+ except (OSError, ValueError, subprocess.TimeoutExpired) as error:
1510
+ proof.update(status="unknown", reason=str(error))
1511
+ finally:
1512
+ try:
1513
+ proof["cleanup"] = cleanup(project=project, owner=owner, expected_runtime=expected)
1514
+ except (OSError, ValueError, subprocess.TimeoutExpired) as error:
1515
+ proof["cleanup"] = {"status": "incomplete", "error": str(error)}
1516
+ if _native_object(proof["cleanup"], "source proof cleanup").get("status") != "complete":
1517
+ proof.update(status="unknown", reason="native source qualification cleanup incomplete")
1518
+ codex.write_json(destination / "qualification.json", proof)
1519
+ return proof
1520
+
1521
+
1522
+ def _prepare_native_task(
1523
+ *,
1524
+ task_dir: Path,
1525
+ destination: Path,
1526
+ harness: str,
1527
+ version: str,
1528
+ limits: dict[str, int],
1529
+ resolved: _NativePreparation | None = None,
1530
+ ) -> dict[str, object]:
1531
+ """Freeze native task, installed CLI, starting source and separate grader once."""
1532
+ admission = resolved.admission if resolved else validate_native_task(task_dir)
1533
+ version = _normalize_version(version)
1534
+ _cli_guard(harness, version)
1535
+ expected = (
1536
+ resolved.runtime
1537
+ if resolved
1538
+ else runtime_identity(cpus=limits["cpus"], memory_mb=limits["memory_mb"])
1539
+ )
1540
+ destination.mkdir(parents=True, exist_ok=False)
1541
+ artifacts = destination / "artifacts"
1542
+ artifacts.mkdir()
1543
+ frozen = destination / "task"
1544
+ shutil.copytree(task_dir, frozen)
1545
+ if native_task_assets(frozen) != admission["task_assets_sha256"]:
1546
+ raise ValueError("Native task changed while freezing preparation inputs")
1547
+ task_type = importlib.import_module("harbor.models.task.task").Task
1548
+ task = task_type(frozen)
1549
+ modes = importlib.import_module("harbor.models.task.verifier_mode")
1550
+ definition = modes.resolve_verifier_environment_definition(task.config, task.paths)
1551
+ base = (
1552
+ resolved.base
1553
+ if resolved
1554
+ else _native_image(
1555
+ directory=task.paths.environment_dir,
1556
+ image=task.config.environment.docker_image,
1557
+ expected=expected,
1558
+ )
1559
+ )
1560
+ grader = (
1561
+ resolved.grader
1562
+ if resolved
1563
+ else _native_image(
1564
+ directory=definition.directory, image=definition.config.docker_image, expected=expected
1565
+ )
1566
+ )
1567
+ test_delivery = "native-image"
1568
+ if not definition.bundled_tests:
1569
+ grader = _bundle_native_tests(tests=task.paths.tests_dir, grader=grader, expected=expected)
1570
+ test_delivery = "native-upload-frozen-into-image"
1571
+ task.config.environment.docker_image = base["image"]
1572
+ verifier_config = definition.config.model_copy(update={"docker_image": grader["image"]})
1573
+ task.config.verifier.environment = verifier_config
1574
+ (frozen / "task.toml").write_text(task.config.model_dump_toml())
1575
+ owner = uuid4().hex
1576
+ project = "pluginbench-" + owner + "__env"
1577
+ config = _native_config(
1578
+ task_dir=frozen,
1579
+ area=destination,
1580
+ owner=owner,
1581
+ harness=harness,
1582
+ version=version,
1583
+ limits=dict(limits),
1584
+ timeout=limits.get("setup_timeout", 600),
1585
+ )
1586
+ config["install_only"] = True
1587
+ config["pluginbench_boundary"] = {
1588
+ "area": str(destination.resolve()),
1589
+ "runtime": expected,
1590
+ "image": base,
1591
+ "harness": harness,
1592
+ "version": version,
1593
+ "install_only": True,
1594
+ }
1595
+ failure: BaseException | None = None
1596
+ receipt: dict[str, object] = {"status": "incomplete", "cleanup": {"status": "incomplete"}}
1597
+ try:
1598
+ worker = _native_process(
1599
+ area=destination,
1600
+ config=config,
1601
+ expected=expected,
1602
+ harness=harness,
1603
+ timeout=_native_worker_budget(task_dir=frozen, prepare=True),
1604
+ install_only=True,
1605
+ )
1606
+ result = _native_object(worker["result"], "native installation result")
1607
+ if worker.get("session_id") != project or result.get("exception_info") is not None:
1608
+ raise ValueError(
1609
+ f"Native agent installation did not complete: {result.get('exception_info')}"
1610
+ )
1611
+ container = _native_container(
1612
+ project=project,
1613
+ trial_dir=artifacts / "harbor" / ("pluginbench-" + owner),
1614
+ expected=expected,
1615
+ image_id=base["image_id"],
1616
+ )
1617
+ container_id = str(container["Id"])
1618
+ fixture = destination / "fixture"
1619
+ export = export_native_source(
1620
+ destination=fixture,
1621
+ project=project,
1622
+ trial_dir=artifacts / "harbor" / ("pluginbench-" + owner),
1623
+ workdir=base["workdir"],
1624
+ expected=expected,
1625
+ image_id=base["image_id"],
1626
+ fresh_source=True,
1627
+ )
1628
+ installed_id = _docker("commit", container_id, expected_runtime=expected).strip()
1629
+ installed = _native_image(
1630
+ directory=frozen / "environment", image=installed_id, expected=expected
1631
+ )
1632
+ task.config.environment.docker_image = installed["image"]
1633
+ (frozen / "task.toml").write_text(task.config.model_dump_toml())
1634
+ qualification = _qualify_native_source(
1635
+ task_dir=frozen,
1636
+ fixture=fixture,
1637
+ coding_image=installed,
1638
+ grader=grader,
1639
+ expected=expected,
1640
+ destination=artifacts / "source-qualification",
1641
+ )
1642
+ receipt["source_qualification"] = qualification
1643
+ qualification_cleanup = qualification.get("cleanup")
1644
+ if (
1645
+ isinstance(qualification_cleanup, dict)
1646
+ and qualification_cleanup.get("status") != "complete"
1647
+ ):
1648
+ raise ValueError("Native source qualification cleanup incomplete")
1649
+ receipt.update(
1650
+ status="complete",
1651
+ task_dir=str(frozen.resolve()),
1652
+ fixture_dir=str(fixture.resolve()),
1653
+ image_identity=installed,
1654
+ toolchain_identity={
1655
+ "image_id": installed["image_id"],
1656
+ "harness": harness,
1657
+ "harness_version": version,
1658
+ "platform": installed["platform"],
1659
+ "install_policy": "harbor-native-install-only-v1",
1660
+ "base_image_identity": base,
1661
+ },
1662
+ grader_image_identity=grader,
1663
+ verifier_test_delivery=test_delivery,
1664
+ trusted_tests_sha256=native_task_assets(task.paths.tests_dir),
1665
+ workdir=base["workdir"],
1666
+ source_digest=export["snapshot_digest"],
1667
+ task_assets_sha256=native_task_assets(frozen),
1668
+ original_task_assets_sha256=admission["task_assets_sha256"],
1669
+ resources=limits,
1670
+ isolation=expected,
1671
+ transport_policy={
1672
+ "name": NATIVE_TRANSPORT_POLICY,
1673
+ "provider_hosts": NATIVE_PROVIDER_HOSTS[harness],
1674
+ },
1675
+ preparation_policy=_PREPARATION_POLICY,
1676
+ source_policy=submissions.SOURCE_POLICY,
1677
+ source_qualification=qualification,
1678
+ )
1679
+ except BaseException as error:
1680
+ failure = error
1681
+ receipt["error"] = str(error)
1682
+ finally:
1683
+ try:
1684
+ receipt["cleanup"] = cleanup(project=project, owner=owner, expected_runtime=expected)
1685
+ except (OSError, ValueError, subprocess.TimeoutExpired) as error:
1686
+ receipt["cleanup"] = {"status": "incomplete", "error": str(error)}
1687
+ if failure is None:
1688
+ failure = error
1689
+ else:
1690
+ failure.add_note("Native preparation cleanup failed: " + str(error))
1691
+ codex.write_json(artifacts / "native-preparation.json", receipt)
1692
+ if failure is not None:
1693
+ raise failure
1694
+ return receipt
1695
+
1696
+
1697
+ def prepare_native_task(
1698
+ *,
1699
+ task_dir: Path,
1700
+ destination: Path,
1701
+ harness: str,
1702
+ version: str,
1703
+ limits: dict[str, int],
1704
+ cache: Path | None = None,
1705
+ ) -> dict[str, object]:
1706
+ """Reuse verified prepared source/images across evaluations with identical inputs."""
1707
+ version = _normalize_version(version)
1708
+ _cli_guard(harness, version)
1709
+ if cache is None:
1710
+ return _prepare_native_task(
1711
+ task_dir=task_dir,
1712
+ destination=destination,
1713
+ harness=harness,
1714
+ version=version,
1715
+ limits=limits,
1716
+ )
1717
+ admission = validate_native_task(task_dir)
1718
+ expected = runtime_identity(cpus=limits["cpus"], memory_mb=limits["memory_mb"])
1719
+ task = importlib.import_module("harbor.models.task.task").Task(task_dir)
1720
+ modes = importlib.import_module("harbor.models.task.verifier_mode")
1721
+ definition = modes.resolve_verifier_environment_definition(task.config, task.paths)
1722
+ base = _native_image(
1723
+ directory=task.paths.environment_dir,
1724
+ image=task.config.environment.docker_image,
1725
+ expected=expected,
1726
+ )
1727
+ grader = _native_image(
1728
+ directory=definition.directory, image=definition.config.docker_image, expected=expected
1729
+ )
1730
+ agent_module = importlib.import_module(
1731
+ "harbor.agents.installed.codex"
1732
+ if harness == "codex"
1733
+ else "harbor.agents.installed.claude_code"
1734
+ )
1735
+ agent_type = getattr(agent_module, "Codex" if harness == "codex" else "ClaudeCode")
1736
+ inputs: dict[str, object] = {
1737
+ "task_assets_sha256": admission["task_assets_sha256"],
1738
+ "base_image": base,
1739
+ "grader_base_image": grader,
1740
+ "harness": harness,
1741
+ "version": _normalize_version(version),
1742
+ "resources": limits,
1743
+ "runtime": expected,
1744
+ "install_sha256": hashlib.sha256(
1745
+ inspect.getsource(agent_type.install).encode()
1746
+ ).hexdigest(),
1747
+ "policy": _PREPARATION_POLICY,
1748
+ "source_policy": submissions.SOURCE_POLICY,
1749
+ "transport_policy": {
1750
+ "name": NATIVE_TRANSPORT_POLICY,
1751
+ "provider_hosts": NATIVE_PROVIDER_HOSTS[harness],
1752
+ },
1753
+ }
1754
+ # Keyed by the runtime contract, so recorded and fit runtime fields never miss the cache.
1755
+ contract = _preparation_contract(inputs)
1756
+ key = hashlib.sha256(json.dumps(contract, sort_keys=True).encode()).hexdigest()
1757
+ if cache.is_symlink():
1758
+ raise ValueError("Native preparation cache must not follow symlinks")
1759
+ cache.mkdir(parents=True, exist_ok=True)
1760
+ lock_path = cache / (key + ".lock")
1761
+ cached = cache / key
1762
+ if lock_path.is_symlink() or cached.is_symlink():
1763
+ raise ValueError("Native preparation cache entries must not follow symlinks")
1764
+ with lock_path.open("a") as lock, ExitStack() as held:
1765
+ try:
1766
+ fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
1767
+ except BlockingIOError as error:
1768
+ raise ValueError(
1769
+ "Native preparation inputs are in use by another evaluation"
1770
+ ) from error
1771
+ saved = None if cached.exists() else _saved_preparation(cache, contract, expected, held)
1772
+ if saved is None:
1773
+ if not cached.exists():
1774
+ with tempfile.TemporaryDirectory(prefix=".native-prepare-", dir=cache) as temporary:
1775
+ prepared = Path(temporary) / "prepared"
1776
+ try:
1777
+ _prepare_native_task(
1778
+ task_dir=task_dir,
1779
+ destination=prepared,
1780
+ harness=harness,
1781
+ version=version,
1782
+ limits=limits,
1783
+ resolved=_NativePreparation(admission, expected, base, grader),
1784
+ )
1785
+ except BaseException as error:
1786
+ try:
1787
+ destination.mkdir(parents=True, exist_ok=False)
1788
+ if (prepared / "artifacts").exists():
1789
+ shutil.copytree(
1790
+ prepared / "artifacts", destination / "artifacts", symlinks=True
1791
+ )
1792
+ codex.write_json(
1793
+ destination / "artifacts/preparation-failure.json",
1794
+ {
1795
+ "status": "failed",
1796
+ "error": str(error),
1797
+ "preparation_key": key,
1798
+ "inputs": inputs,
1799
+ },
1800
+ )
1801
+ except (OSError, ValueError) as evidence_error:
1802
+ error.add_note(
1803
+ "Could not preserve native preparation failure evidence: "
1804
+ + str(evidence_error)
1805
+ )
1806
+ raise
1807
+ manifest = _native_asset_inventory(prepared)
1808
+ codex.write_json(prepared / "cache.json", {"inputs": inputs, "files": manifest})
1809
+ prepared.rename(cached)
1810
+ saved = cached, _cached_preparation(cached, contract, expected)
1811
+ assert_prepared_images(saved[1], expected, label=f"Prepared cache entry {cached}")
1812
+ cached, receipt = saved
1813
+ shutil.copytree(cached, destination)
1814
+ # Docker work for this run uses the runtime it runs on; the entry's is provenance.
1815
+ receipt.update(
1816
+ task_dir=str((destination / "task").resolve()),
1817
+ fixture_dir=str((destination / "fixture").resolve()),
1818
+ preparation_key=cached.name,
1819
+ isolation=expected,
1820
+ preparation_isolation=receipt["isolation"],
1821
+ )
1822
+ receipt["preparation_identity"] = {
1823
+ name: value
1824
+ for name, value in receipt.items()
1825
+ if name not in {"task_dir", "fixture_dir", "cleanup", "preparation_identity"}
1826
+ }
1827
+ codex.write_json(destination / "artifacts/native-preparation.json", receipt)
1828
+ return receipt
1829
+
1830
+
1831
+ def _preparation_contract(inputs: dict[str, Any]) -> dict[str, Any]:
1832
+ return {**inputs, "runtime": runtime_contract(_native_object(inputs["runtime"], "runtime"))}
1833
+
1834
+
1835
+ def _cached_preparation(
1836
+ cached: Path, contract: dict[str, Any], expected: dict[str, object]
1837
+ ) -> dict[str, Any]:
1838
+ """The receipt of a complete cache entry prepared from equivalent inputs; raise if not."""
1839
+ packet = _native_object(json.loads((cached / "cache.json").read_text()), "native cache packet")
1840
+ if _preparation_contract(_native_object(packet["inputs"], "native cache inputs")) != contract:
1841
+ raise ValueError("Native preparation cache inputs changed")
1842
+ files = _native_object(packet["files"], "native cache files")
1843
+ if _native_asset_inventory(cached, exclude="cache.json") != files:
1844
+ raise ValueError("Native preparation cache evidence changed")
1845
+ receipt = _native_object(
1846
+ json.loads((cached / "artifacts/native-preparation.json").read_text()),
1847
+ "cached native preparation",
1848
+ )
1849
+ for name in ("image_identity", "grader_image_identity"):
1850
+ image = _native_object(receipt.get(name), "cached native image")
1851
+ if not all(isinstance(image.get(key), str) for key in ("image", "image_id")):
1852
+ raise ValueError("Native preparation cache has incomplete or invalid proof")
1853
+ if (
1854
+ receipt["status"] != "complete"
1855
+ or _native_object(receipt["cleanup"], "cached cleanup")["status"] != "complete"
1856
+ or runtime_contract(_native_object(receipt["isolation"], "cached runtime"))
1857
+ != runtime_contract(expected)
1858
+ or inspect_snapshot(cached / "fixture")["digest"] != receipt["source_digest"]
1859
+ or native_task_assets(cached / "task") != receipt["task_assets_sha256"]
1860
+ ):
1861
+ raise ValueError("Native preparation cache has incomplete or invalid proof")
1862
+ return receipt
1863
+
1864
+
1865
+ def _saved_preparation(
1866
+ cache: Path, contract: dict[str, Any], expected: dict[str, object], held: ExitStack
1867
+ ) -> tuple[Path, dict[str, Any]] | None:
1868
+ """An entry saved under another key, as by 942f12c, whose inputs are equivalent.
1869
+
1870
+ Candidates are tried in name order. One that is in use, invalid, or names source this
1871
+ build does not know is skipped: it is not this preparation. So is one whose images the
1872
+ current Docker engine no longer holds; if no candidate is usable for that reason, the
1873
+ first such error is raised. The chosen entry stays share-locked until `held` closes.
1874
+ """
1875
+ unavailable: ValueError | None = None
1876
+ for candidate in sorted(cache.iterdir()):
1877
+ lock_path = cache / (candidate.name + ".lock")
1878
+ if (
1879
+ candidate.name.startswith(".")
1880
+ or candidate.suffix == ".lock"
1881
+ or candidate.is_symlink()
1882
+ or not candidate.is_dir()
1883
+ or lock_path.is_symlink()
1884
+ ):
1885
+ continue
1886
+ try:
1887
+ lock = lock_path.open("a")
1888
+ except OSError:
1889
+ continue
1890
+ kept = False
1891
+ try:
1892
+ try:
1893
+ fcntl.flock(lock, fcntl.LOCK_SH | fcntl.LOCK_NB)
1894
+ receipt = _cached_preparation(candidate, contract, expected)
1895
+ except (OSError, ValueError, KeyError, TypeError):
1896
+ continue
1897
+ try:
1898
+ assert_prepared_images(
1899
+ receipt, expected, label=f"Prepared cache entry {candidate}"
1900
+ )
1901
+ except ValueError as error:
1902
+ unavailable = unavailable or error
1903
+ continue
1904
+ held.enter_context(lock)
1905
+ kept = True # `held` closes it from now on.
1906
+ return candidate, receipt
1907
+ finally:
1908
+ if not kept:
1909
+ lock.close()
1910
+ if unavailable is not None:
1911
+ raise unavailable
1912
+ return None
1913
+
1914
+
1915
+ # The neutral git identity PluginBench supplies to a task whose image and environment set none.
1916
+ SUPPLIED_GIT_IDENTITY = {"name": "PluginBench Agent", "email": "agent@pluginbench.invalid"}
1917
+ # The probe's exit status when git has no author or committer identity (git itself uses 1 and 128).
1918
+ _NO_GIT_IDENTITY_EXIT = "3"
1919
+ # The same, in a task that already sets the variable the supplied identity travels in.
1920
+ _GIT_PARAMETERS_SET_EXIT = "4"
1921
+
1922
+
1923
+ def supplied_git_env(identity: dict[str, object]) -> dict[str, str]:
1924
+ """The agent environment variables that carry the git identity PluginBench supplied, if any.
1925
+
1926
+ A task with its own identity has no `supplied_git_identity` and gets nothing. The probe and
1927
+ every agent launch take the identity from here, so they set the same variables. The probe
1928
+ supplies an identity only to a task that does not set GIT_CONFIG_PARAMETERS itself, so this
1929
+ never replaces a task's own git settings.
1930
+ """
1931
+ if "supplied_git_identity" not in identity:
1932
+ return {}
1933
+ supplied = _native_object(identity["supplied_git_identity"], "supplied git identity")
1934
+ # The variable git uses to pass `git -c` settings, in the quoted `key=value` form every git
1935
+ # version reads. Not GIT_CONFIG_KEY_n or GIT_AUTHOR_*: Harbor treats a name containing KEY or
1936
+ # AUTH as a secret and redacts its value from all trial output.
1937
+ return {
1938
+ "GIT_CONFIG_PARAMETERS": f"'user.name={supplied['name']}' 'user.email={supplied['email']}'"
1939
+ }
1940
+
1941
+
1942
+ def probe_coding_environment(identity: dict[str, object]) -> dict[str, object]:
1943
+ """No-model proof that an agent can commit in the frozen coding image.
1944
+
1945
+ Graded patches are `git diff BASE HEAD`, so an image where `git commit` fails makes every
1946
+ attempt an empty patch. The first probe uses only the task's own environment and agent user,
1947
+ exactly as Harbor starts the agent. Only when git there has no identity does a second probe
1948
+ add PluginBench's neutral identity; the receipt then names it in `supplied_git_identity`.
1949
+ """
1950
+ receipt, exit_code = _git_probe(identity)
1951
+ if exit_code != _NO_GIT_IDENTITY_EXIT:
1952
+ return receipt
1953
+ supplied = dict(SUPPLIED_GIT_IDENTITY)
1954
+ second, _ = _git_probe({**identity, "supplied_git_identity": supplied})
1955
+ if second["status"] != "passed":
1956
+ second["reason"] = "with the git identity PluginBench supplied, " + str(second["reason"])
1957
+ return {**second, "supplied_git_identity": supplied, "task_identity_probe": receipt}
1958
+
1959
+
1960
+ def _git_probe(identity: dict[str, object]) -> tuple[dict[str, object], str | None]:
1961
+ """One commit probe and the container's exit status (None if it did not run to the end)."""
1962
+ expected = _native_object(identity["isolation"], "probe runtime")
1963
+ limits = _native_object(expected["resources"], "probe resources")
1964
+ image = _native_object(identity["image_identity"], "probe coding image")
1965
+ task = importlib.import_module("harbor.models.task.task").Task(Path(str(identity["task_dir"])))
1966
+ resolve_env = importlib.import_module("harbor.utils.env").resolve_env_vars
1967
+ workdir = shlex.quote(str(identity["workdir"]))
1968
+ git = "git -c safe.directory=" + workdir
1969
+ supplied_env = supplied_git_env(identity)
1970
+ # Agent variables replace the task's own, so a task that sets GIT_CONFIG_PARAMETERS would
1971
+ # lose its settings to a supplied identity. The task-only probe reports that case apart.
1972
+ no_identity = f"exit {_NO_GIT_IDENTITY_EXIT}"
1973
+ if not supplied_env:
1974
+ no_identity = (
1975
+ f'test -z "${{GIT_CONFIG_PARAMETERS:-}}" || exit {_GIT_PARAMETERS_SET_EXIT}; '
1976
+ + no_identity
1977
+ )
1978
+ # `git var` applies the same identity rules as `git commit`; its own message stays in the logs.
1979
+ command = (
1980
+ f"exec 2>&1; set -eu; cd {workdir}; before=$({git} rev-parse HEAD); "
1981
+ f"{git} var GIT_AUTHOR_IDENT >/dev/null && {git} var GIT_COMMITTER_IDENT >/dev/null "
1982
+ f"|| {{ {no_identity}; }}; "
1983
+ f"{git} commit --allow-empty --no-verify -q -m pluginbench-git-probe; "
1984
+ f'test "$({git} rev-parse HEAD)" != "$before"; {git} var GIT_AUTHOR_IDENT'
1985
+ )
1986
+ owner = uuid4().hex
1987
+ project = "pluginbench-git-probe-" + owner
1988
+ arguments = [
1989
+ "create",
1990
+ "--label",
1991
+ "com.docker.compose.project=" + project,
1992
+ "--label",
1993
+ _OWNER_LABEL + "=" + owner,
1994
+ "--cpus",
1995
+ str(limits["cpus"]),
1996
+ "--memory",
1997
+ str(limits["memory_mb"]) + "m",
1998
+ "--network",
1999
+ "none",
2000
+ "--entrypoint",
2001
+ "bash",
2002
+ ]
2003
+ environment = {**resolve_env(task.config.environment.env), **supplied_env}
2004
+ for key, value in sorted(environment.items()):
2005
+ arguments.extend(["--env", f"{key}={value}"])
2006
+ if task.config.agent.user is not None:
2007
+ arguments.extend(["--user", str(task.config.agent.user)])
2008
+ receipt: dict[str, object] = {"status": "failed", "image_id": image["image_id"]}
2009
+ exit_code: str | None = None
2010
+ try:
2011
+ container = _docker(
2012
+ *arguments, str(image["image_id"]), "-c", command, expected_runtime=expected
2013
+ ).strip()
2014
+ # Not `start --attach`: that exits with the container's status, and a failed commit
2015
+ # would then surface as "docker start failed" without the reason in the logs.
2016
+ _docker("start", container, expected_runtime=expected)
2017
+ exit_code = _docker("wait", container, timeout=60, expected_runtime=expected).strip()
2018
+ output = _docker("logs", container, expected_runtime=expected).strip()
2019
+ if exit_code == "0":
2020
+ receipt.update(status="passed", author=output.splitlines()[-1] if output else None)
2021
+ elif exit_code == _NO_GIT_IDENTITY_EXIT:
2022
+ receipt["reason"] = f"git has no author or committer identity in the coding image: {output}"
2023
+ elif exit_code == _GIT_PARAMETERS_SET_EXIT:
2024
+ receipt["reason"] = (
2025
+ "git has no author or committer identity in the coding image, and PluginBench "
2026
+ "cannot supply one because the task sets GIT_CONFIG_PARAMETERS. Add user.name "
2027
+ f"and user.email to the task's git settings: {output}"
2028
+ )
2029
+ else:
2030
+ receipt["reason"] = (
2031
+ f"git commit failed in the coding image (exit {exit_code}): {output}"
2032
+ )
2033
+ except (OSError, ValueError, subprocess.TimeoutExpired) as error:
2034
+ receipt["reason"] = str(error)
2035
+ finally:
2036
+ try:
2037
+ receipt["cleanup"] = cleanup(project=project, owner=owner, expected_runtime=expected)
2038
+ except (OSError, ValueError, subprocess.TimeoutExpired) as error:
2039
+ receipt["cleanup"] = {"status": "incomplete", "error": str(error)}
2040
+ receipt.update(status="failed", reason="git probe cleanup incomplete: " + str(error))
2041
+ exit_code = None
2042
+ return receipt, exit_code
2043
+
2044
+
2045
+ def binary_native_reward(verifier: object) -> bool | None:
2046
+ """A native verifier's pass (1) or fail (0); None when its reward is not binary."""
2047
+ rewards = verifier.get("rewards") if isinstance(verifier, dict) else None
2048
+ if isinstance(rewards, dict) and ("reward" in rewards or len(rewards) == 1):
2049
+ reward = rewards["reward"] if "reward" in rewards else next(iter(rewards.values()))
2050
+ if type(reward) in {int, float} and reward in (0, 1):
2051
+ return bool(reward)
2052
+ return None
2053
+
2054
+
2055
+ _REFERENCE_AGENT_TIMEOUT = 900
2056
+
2057
+
2058
+ def run_reference_agents(identity: dict[str, object], destination: Path) -> dict[str, object]:
2059
+ """Run Harbor's oracle and nop agents through the frozen coding, patch and verifier path.
2060
+
2061
+ No credentials enter these trials (install-only environment), so no model can be called.
2062
+ Each trial's stopped source is exported and its graded patch reconstructed over the frozen
2063
+ fixture with the same functions paid attempts use, so a failure there blocks the check.
2064
+ The caller judges the results: oracle needs a nonempty patch and reward 1, nop reward 0.
2065
+ """
2066
+ task_dir = Path(str(identity["task_dir"]))
2067
+ expected = _native_object(identity["isolation"], "reference runtime")
2068
+ toolchain = _native_object(identity["toolchain_identity"], "reference toolchain")
2069
+ solve = importlib.import_module("harbor.models.task.task").Task(task_dir).paths.solve_path
2070
+ agent_env = supplied_git_env(identity)
2071
+ results: dict[str, object] = {}
2072
+ for agent in ("oracle", "nop"):
2073
+ if agent == "oracle" and not solve.is_file():
2074
+ results[agent] = {
2075
+ "status": "not_configured",
2076
+ "reason": "task ships no solution/solve.sh",
2077
+ }
2078
+ continue
2079
+ area = destination / agent
2080
+ area.mkdir(parents=True)
2081
+ owner = uuid4().hex
2082
+ name = "pluginbench-" + owner
2083
+ config = _native_config(
2084
+ task_dir=task_dir,
2085
+ area=area,
2086
+ owner=owner,
2087
+ harness=str(toolchain["harness"]),
2088
+ version=str(toolchain["harness_version"]),
2089
+ limits=_native_object(expected["resources"], "reference resources"),
2090
+ timeout=_REFERENCE_AGENT_TIMEOUT,
2091
+ )
2092
+ # Harbor's built-in agents take no coding-CLI settings or provider hosts; the oracle
2093
+ # commits with the same git identity a coding agent gets.
2094
+ config["agent"] = {
2095
+ "name": agent,
2096
+ "override_timeout_sec": _REFERENCE_AGENT_TIMEOUT,
2097
+ "kwargs": {},
2098
+ **({"env": agent_env} if agent_env else {}),
2099
+ }
2100
+ row: dict[str, object] = {"status": "failed"}
2101
+ result: dict[str, object] = {}
2102
+ export: dict[str, object] | None = None
2103
+ try:
2104
+ worker = _native_process(
2105
+ area=area,
2106
+ config=config,
2107
+ expected=expected,
2108
+ harness=str(toolchain["harness"]),
2109
+ timeout=_native_worker_budget(
2110
+ task_dir=task_dir, agent_seconds=_REFERENCE_AGENT_TIMEOUT
2111
+ ),
2112
+ install_only=True,
2113
+ )
2114
+ if worker.get("session_id") != name + "__env":
2115
+ raise ValueError("Reference agent returned a foreign trial identity")
2116
+ result = _native_object(worker["result"], "reference agent result")
2117
+ except (OSError, ValueError, subprocess.TimeoutExpired) as error:
2118
+ row["reason"] = str(error)
2119
+ finally:
2120
+ # Freeze the stopped source before cleanup, exactly as a paid attempt does.
2121
+ try:
2122
+ export = _native_submission(
2123
+ area=area, name=name, identity=identity, expected=expected
2124
+ )
2125
+ codex.write_json(area / "artifacts/submission-source.json", export)
2126
+ except (OSError, ValueError, subprocess.TimeoutExpired) as error:
2127
+ row.setdefault("reason", "stopped-source export failed: " + str(error))
2128
+ row["cleanup"] = _native_cleanup_projects(name=name, owner=owner, expected=expected)
2129
+ if export is not None and "reason" not in row:
2130
+ try:
2131
+ row.update(
2132
+ status="complete",
2133
+ exception_info=result.get("exception_info"),
2134
+ verifier_result=result.get("verifier_result"),
2135
+ **_admit_reference_submission(
2136
+ area=area, name=name, identity=identity, export=export
2137
+ ),
2138
+ )
2139
+ except (OSError, ValueError) as error:
2140
+ row.update(status="failed", reason="graded patch admission failed: " + str(error))
2141
+ codex.write_json(area / "reference-agent.json", row)
2142
+ results[agent] = row
2143
+ return results
2144
+
2145
+
2146
+ def _admit_reference_submission(
2147
+ *, area: Path, name: str, identity: dict[str, object], export: dict[str, object]
2148
+ ) -> dict[str, object]:
2149
+ """Admit a reference trial's export and graded patch as the study admits a paid attempt's."""
2150
+ artifacts = area / "artifacts"
2151
+ (artifacts / "harbor" / name).rename(artifacts / "harbor/trial")
2152
+ execution: dict[str, Any] = {
2153
+ "submission_export_digest": export["snapshot_digest"],
2154
+ "native_artifact_hashes": _native_artifact_hashes(artifacts / "harbor/trial/artifacts"),
2155
+ }
2156
+ submissions.validate_export(
2157
+ diagnostic=area / "workspace", artifacts=artifacts, execution=execution
2158
+ )
2159
+ patch = artifacts / "harbor/trial/artifacts" / submissions.PATCH_ARTIFACT
2160
+ qualification = identity.get("source_qualification")
2161
+ if identity.get("source_policy") != submissions.SOURCE_POLICY or not (
2162
+ isinstance(qualification, dict) and qualification.get("status") == "complete"
2163
+ ):
2164
+ # The submission_source readiness row already reports why; paid attempts fail the same way.
2165
+ reconstruction: dict[str, object] = {
2166
+ "status": "skipped",
2167
+ "reason": "submission source is not qualified",
2168
+ }
2169
+ else:
2170
+ reconstruction = submissions.reconstruct(
2171
+ before=Path(str(identity["fixture_dir"])),
2172
+ task_dir=Path(str(identity["task_dir"])),
2173
+ identity=identity,
2174
+ artifacts=artifacts,
2175
+ execution=execution,
2176
+ destination=area / "snapshot",
2177
+ )
2178
+ return {
2179
+ "patch_bytes": patch.stat().st_size if patch.is_file() else None,
2180
+ "export_digest": export["snapshot_digest"],
2181
+ "reconstruction": reconstruction,
2182
+ }
2183
+
2184
+
2185
+ def _native_cleanup_projects(
2186
+ *, name: str, owner: str, expected: dict[str, object]
2187
+ ) -> dict[str, object]:
2188
+ projects: dict[str, object] = {}
2189
+ for project in (name + "__env", name + "__verifier__trial"):
2190
+ try:
2191
+ projects[project] = cleanup(project=project, owner=owner, expected_runtime=expected)
2192
+ except (OSError, ValueError, subprocess.TimeoutExpired) as error:
2193
+ projects[project] = {"status": "incomplete", "error": str(error)}
2194
+ return {
2195
+ "status": "complete"
2196
+ if all(
2197
+ _native_object(value, "native cleanup")["status"] == "complete"
2198
+ for value in projects.values()
2199
+ )
2200
+ else "incomplete",
2201
+ "projects": projects,
2202
+ }
2203
+
2204
+
2205
+ def _native_submission(
2206
+ *, area: Path, name: str, identity: dict[str, object], expected: dict[str, object]
2207
+ ) -> dict[str, object]:
2208
+ receipt_path = area / "artifacts/submission-source.json"
2209
+ if receipt_path.is_file():
2210
+ receipt = _native_object(json.loads(receipt_path.read_text()), "native submission receipt")
2211
+ else:
2212
+ image = _native_object(identity["image_identity"], "native coding image")
2213
+ container = _native_container(
2214
+ project=name + "__env",
2215
+ trial_dir=area / "artifacts/harbor" / name,
2216
+ expected=expected,
2217
+ image_id=str(image["image_id"]),
2218
+ )
2219
+ _docker("stop", "--time", "10", str(container["Id"]), expected_runtime=expected)
2220
+ receipt = export_native_source(
2221
+ destination=area / "workspace",
2222
+ project=name + "__env",
2223
+ trial_dir=area / "artifacts/harbor" / name,
2224
+ workdir=str(identity["workdir"]),
2225
+ expected=expected,
2226
+ image_id=str(image["image_id"]),
2227
+ fresh_source=True,
2228
+ agent_workspace=True,
2229
+ )
2230
+ if inspect_snapshot(area / "workspace")["digest"] != receipt["snapshot_digest"]:
2231
+ raise ValueError("Frozen native submission changed before measurement")
2232
+ if (
2233
+ receipt.get("source_metadata_digest") is not None
2234
+ and native_task_assets(area / "workspace") != receipt["source_metadata_digest"]
2235
+ ):
2236
+ raise ValueError("Frozen native submission metadata changed before measurement")
2237
+ if identity.get("preparation_policy") == _PREPARATION_POLICY and not isinstance(
2238
+ receipt.get("source_metadata_digest"), str
2239
+ ):
2240
+ raise ValueError("Frozen native submission metadata proof unavailable")
2241
+ return receipt
2242
+
2243
+
2244
+ def _native_completion(
2245
+ *,
2246
+ area: Path,
2247
+ name: str,
2248
+ execution: dict[str, object],
2249
+ native_result: dict[str, object],
2250
+ receipt: dict[str, object],
2251
+ launch: dict[str, object],
2252
+ failure: BaseException | None,
2253
+ started: float,
2254
+ ) -> None:
2255
+ artifacts = area / "artifacts"
2256
+ host = str(launch["harness"]).removesuffix("-cli")
2257
+ trial_dir = artifacts / "harbor" / name
2258
+ if trial_dir.exists():
2259
+ trial_dir.rename(artifacts / "harbor/trial")
2260
+ if _native_object(receipt["cleanup"], "native cleanup")["status"] == "complete":
2261
+ try:
2262
+ execution.update(
2263
+ _native_evidence(
2264
+ area=area,
2265
+ host=host,
2266
+ effort=str(launch["requested_effort"]),
2267
+ transport=_CODEX_TRANSPORT_VERSION if host == "codex" else None,
2268
+ include_descendants=bool(
2269
+ _native_object(launch.get("capabilities", {}), "coding capabilities").get("subagents")
2270
+ ),
2271
+ )
2272
+ )
2273
+ except (OSError, ValueError) as error:
2274
+ execution["evidence_error"] = str(error)
2275
+ native_error = native_result.get("exception_info")
2276
+ if native_error is not None:
2277
+ execution["error"] = native_error
2278
+ execution.update(
2279
+ native_result=native_result,
2280
+ native_verifier_result=native_result.get("verifier_result"),
2281
+ native_artifact_hashes=_native_artifact_hashes(artifacts / "harbor/trial/artifacts"),
2282
+ timed_out=isinstance(failure, subprocess.TimeoutExpired)
2283
+ or (
2284
+ isinstance(native_error, dict)
2285
+ and native_error.get("exception_type") == "AgentTimeoutError"
2286
+ ),
2287
+ elapsed_seconds=time.monotonic() - started,
2288
+ private_runtime_removed=not (area / "private").exists(),
2289
+ launch=launch,
2290
+ launch_harness_version=codex.observed_cli_version(execution.get("startup"), host=host),
2291
+ )
2292
+ if launch.get("delivery") == native_treatments.DELIVERY_POLICY:
2293
+ try:
2294
+ treatment_receipt = _native_object(
2295
+ json.loads((artifacts / "native-treatment.json").read_text()), "native treatment receipt"
2296
+ )
2297
+ if (treatment_receipt.get("status") != "verified"
2298
+ or treatment_receipt.get("inventory") != launch.get("treatment_inventory")):
2299
+ raise ValueError("native treatment installation was not verified for this attempt")
2300
+ execution["native_treatment"] = treatment_receipt
2301
+ # Command hooks may execute subprocesses outside native session logging.
2302
+ # Preserve native observed usage, but never present it as complete total cost.
2303
+ if _native_object(treatment_receipt.get("hooks"), "native hooks").get("hooks"):
2304
+ execution["native_session_usage"] = execution.get("usage")
2305
+ execution["usage"] = None
2306
+ execution["usage_accounting"] = {
2307
+ "status": "incomplete",
2308
+ "reason": "command-hook subprocess usage is not independently metered",
2309
+ "native_sessions": execution.get("usage_accounting"),
2310
+ }
2311
+ except (OSError, ValueError) as error:
2312
+ execution["treatment_error"] = str(error)
2313
+ execution["successful_native_completion"] = False
2314
+ if failure is not None or native_error is not None:
2315
+ execution["successful_native_completion"] = False
2316
+ execution["exit_code"] = 0 if execution["successful_native_completion"] else 1
2317
+ codex.write_json(artifacts / "runtime-isolation.json", receipt)
2318
+ codex.write_json(artifacts / "execution.json", execution)
2319
+
2320
+
2321
+ def invoke_native(
2322
+ *,
2323
+ area: Path,
2324
+ task_dir: Path,
2325
+ entry: dict[str, object],
2326
+ protocol: dict[str, object],
2327
+ prompt: str,
2328
+ timeout: int,
2329
+ ) -> dict[str, object]:
2330
+ """Run the frozen standard task and its native separate verifier."""
2331
+ identity = _native_object(protocol.get("native_task_identity"), "prepared native task")
2332
+ host = str(entry["host"])
2333
+ treatment = Treatment.model_validate(protocol.get("treatment", {}))
2334
+ capabilities = NativeCapabilities.model_validate(protocol.get("capabilities", {}))
2335
+ native_treatments.admit(treatment, host)
2336
+ if prompt:
2337
+ raise ValueError("native coding task prompts must not contain treatment text")
2338
+ toolchain = _native_object(identity["toolchain_identity"], "native toolchain")
2339
+ if (
2340
+ toolchain["harness"] != host
2341
+ or toolchain["harness_version"] != _normalize_version(str(protocol["harness_version"]))
2342
+ or identity.get("transport_policy")
2343
+ != {"name": NATIVE_TRANSPORT_POLICY, "provider_hosts": NATIVE_PROVIDER_HOSTS.get(host)}
2344
+ ):
2345
+ raise ValueError(
2346
+ "Native task was prepared for different harness or model transport settings"
2347
+ )
2348
+ if native_task_assets(task_dir) != identity["task_assets_sha256"]:
2349
+ raise ValueError("Frozen native task assets changed")
2350
+ validate_native_task(task_dir)
2351
+ expected = assert_runtime(_native_object(protocol["isolation"], "native runtime"))
2352
+ image = _native_object(identity["image_identity"], "native coding image")
2353
+ grader = _native_object(identity["grader_image_identity"], "native grader image")
2354
+ for frozen_image in (image, grader):
2355
+ _assert_native_image(frozen_image, expected)
2356
+ artifacts = area / "artifacts"
2357
+ artifacts.mkdir(exist_ok=True)
2358
+ if (artifacts / "invocation.json").exists():
2359
+ raise ValueError("Native invocation already started; use a fresh area")
2360
+ # Existing study setup copies the fixture; hooks supply the actual before/after.
2361
+ for name in ("before", "workspace"):
2362
+ path = area / name
2363
+ if path.exists():
2364
+ if inspect_snapshot(path)["digest"] != identity["source_digest"]:
2365
+ raise ValueError("Native trial area does not match the prepared fixture")
2366
+ shutil.rmtree(path)
2367
+ owner = uuid4().hex
2368
+ name = "pluginbench-" + owner
2369
+ effort = str(protocol["reasoning"])
2370
+ version = _normalize_version(str(protocol["harness_version"]))
2371
+ models = _native_object(protocol["models"], "native models")
2372
+ limits = _native_object(expected["resources"], "native resources")
2373
+ config = _native_config(
2374
+ task_dir=task_dir,
2375
+ area=area,
2376
+ owner=owner,
2377
+ harness=host,
2378
+ version=version,
2379
+ limits=limits,
2380
+ timeout=timeout,
2381
+ model=str(models[host]),
2382
+ effort=effort,
2383
+ prompt=prompt,
2384
+ treatment=treatment,
2385
+ capabilities=capabilities,
2386
+ agent_env=supplied_git_env(identity),
2387
+ )
2388
+ config["pluginbench_boundary"] = {
2389
+ "treatment": treatment.model_dump(mode="json"),
2390
+ "capabilities": capabilities.model_dump(mode="json"),
2391
+ "area": str(area.resolve()),
2392
+ "runtime": expected,
2393
+ "image": image,
2394
+ "source_digest": identity["source_digest"],
2395
+ "source_metadata_digest": _native_object(
2396
+ identity.get("source_qualification", {}), "source qualification"
2397
+ ).get("fixture_metadata_digest"),
2398
+ "harness": host,
2399
+ "version": version,
2400
+ }
2401
+ launch = {
2402
+ "harness": host + "-cli",
2403
+ "requested_model": models[host],
2404
+ "requested_effort": effort,
2405
+ "harness_version": version,
2406
+ "backend": PROTOCOL,
2407
+ "delivery": native_treatments.DELIVERY_POLICY,
2408
+ "capabilities": capabilities.model_dump(mode="json"),
2409
+ "treatment_inventory": native_treatments.admit(treatment, host),
2410
+ }
2411
+ codex.write_json(
2412
+ artifacts / "invocation.json",
2413
+ {
2414
+ **launch,
2415
+ "task_assets_sha256": identity["task_assets_sha256"],
2416
+ "native_task_identity": identity,
2417
+ # What this attempt ran with: vanilla cache admission checks its Docker client.
2418
+ "runtime": expected,
2419
+ "timeout_seconds": timeout,
2420
+ "delivery": native_treatments.DELIVERY_POLICY,
2421
+ "capabilities": capabilities.model_dump(mode="json"),
2422
+ "treatment_inventory": native_treatments.admit(treatment, host),
2423
+ },
2424
+ )
2425
+ (artifacts / "prompt.txt").write_text(prompt)
2426
+ started = time.monotonic()
2427
+ execution: dict[str, object] = {
2428
+ **codex.parse_events("", [], host, requested_effort=effort),
2429
+ "submission_collected": False,
2430
+ "successful_native_completion": False,
2431
+ }
2432
+ receipt: dict[str, object] = {
2433
+ "export": {"status": "incomplete"},
2434
+ "cleanup": {"status": "incomplete"},
2435
+ }
2436
+ native_result: dict[str, object] = {}
2437
+ failure: BaseException | None = None
2438
+ try:
2439
+ worker = _native_process(
2440
+ area=area,
2441
+ config=config,
2442
+ expected=expected,
2443
+ harness=host,
2444
+ timeout=_native_worker_budget(task_dir=task_dir, agent_seconds=timeout),
2445
+ install_only=False,
2446
+ auth_file=Path(str(protocol["auth_file"])) if protocol.get("auth_file") else None,
2447
+ )
2448
+ if worker.get("session_id") != name + "__env":
2449
+ raise ValueError("Native worker returned a foreign project identity")
2450
+ native_result = _native_object(worker["result"], "native trial result")
2451
+ if native_result.get("verifier_result") is not None:
2452
+ verifier_container = _native_container(
2453
+ project=name + "__verifier__trial",
2454
+ trial_dir=artifacts / "harbor" / name,
2455
+ expected=expected,
2456
+ image_id=str(grader["image_id"]),
2457
+ )
2458
+ receipt["verifier"] = {
2459
+ "container_id": verifier_container["Id"],
2460
+ "image_id": verifier_container["Image"],
2461
+ "state": stopped_container_state(verifier_container.get("State")),
2462
+ "resources": verifier_container["HostConfig"],
2463
+ }
2464
+ except BaseException as error:
2465
+ failure = error
2466
+ execution["error"] = f"{type(error).__name__}: {error}"
2467
+ finally:
2468
+ try:
2469
+ receipt["export"] = _native_submission(
2470
+ area=area, name=name, identity=identity, expected=expected
2471
+ )
2472
+ execution.update(
2473
+ submission_collected=True,
2474
+ submission_export_digest=_native_object(receipt["export"], "native export")[
2475
+ "snapshot_digest"
2476
+ ],
2477
+ )
2478
+ except (OSError, ValueError, subprocess.TimeoutExpired) as error:
2479
+ execution["submission_error"] = str(error)
2480
+ receipt["cleanup"] = _native_cleanup_projects(name=name, owner=owner, expected=expected)
2481
+ if _native_object(receipt["cleanup"], "native cleanup")["status"] != "complete":
2482
+ execution["submission_collected"] = False
2483
+ cleanup_error = ValueError(
2484
+ "Native cleanup incomplete: " + json.dumps(receipt["cleanup"])
2485
+ )
2486
+ if failure is None:
2487
+ failure = cleanup_error
2488
+ else:
2489
+ failure.add_note(str(cleanup_error))
2490
+ _native_completion(
2491
+ area=area,
2492
+ name=name,
2493
+ execution=execution,
2494
+ native_result=native_result,
2495
+ receipt=receipt,
2496
+ launch=launch,
2497
+ failure=failure,
2498
+ started=started,
2499
+ )
2500
+ if failure is not None:
2501
+ raise failure
2502
+ return execution
2503
+
2504
+
2505
+ def run_native_command(
2506
+ *,
2507
+ snapshot: Path,
2508
+ task_identity: dict[str, object],
2509
+ destination: Path,
2510
+ command: list[str],
2511
+ report_path: str,
2512
+ timeout: int,
2513
+ ) -> dict[str, object]:
2514
+ """Run explicitly configured measurement commands in a fresh frozen image."""
2515
+ relative = Path(report_path)
2516
+ if (
2517
+ not command
2518
+ or any(not isinstance(item, str) or not item for item in command)
2519
+ or relative.is_absolute()
2520
+ or ".." in relative.parts
2521
+ or not relative.parts
2522
+ or timeout < 1
2523
+ ):
2524
+ raise ValueError(
2525
+ "Native measurement requires argv, a safe relative report path and a positive timeout"
2526
+ )
2527
+ original = inspect_snapshot(snapshot)
2528
+ original_inventory = _native_asset_inventory(snapshot)
2529
+ if relative.as_posix() in original_inventory or _is_source_path(relative.as_posix()):
2530
+ raise ValueError(
2531
+ "Native measurement report must not overwrite original inputs or source paths"
2532
+ )
2533
+ # Measurement starts no coding agent, so the launcher version does not bind it.
2534
+ expected = assert_runtime(
2535
+ _native_object(task_identity["isolation"], "measurement runtime"), launcher=False
2536
+ )
2537
+ image = _native_object(task_identity["image_identity"], "measurement image")
2538
+ workdir = str(task_identity["workdir"])
2539
+ limits = _native_object(expected["resources"], "measurement resources")
2540
+ owner = uuid4().hex
2541
+ project = "pluginbench-measure-" + owner
2542
+ destination.mkdir(parents=True, exist_ok=True)
2543
+ container_id = ""
2544
+ receipt: dict[str, object] = {
2545
+ "exit_code": None,
2546
+ "timed_out": False,
2547
+ "report_collected": False,
2548
+ "before_digest": original["digest"],
2549
+ "image_id": image["image_id"],
2550
+ "resources": limits,
2551
+ "command": command,
2552
+ "report_path": report_path,
2553
+ "cleanup": {"status": "incomplete"},
2554
+ }
2555
+ failure: BaseException | None = None
2556
+
2557
+ def docker(*arguments: str, limit: int = _DOCKER_TIMEOUT) -> str:
2558
+ return _docker(*arguments, timeout=limit, expected_runtime=expected)
2559
+
2560
+ try:
2561
+ container_id = docker(
2562
+ "create",
2563
+ "--label",
2564
+ "com.docker.compose.project=" + project,
2565
+ "--label",
2566
+ "com.docker.compose.service=main",
2567
+ "--label",
2568
+ _OWNER_LABEL + "=" + owner,
2569
+ "--cpus",
2570
+ str(limits["cpus"]),
2571
+ "--memory",
2572
+ str(limits["memory_mb"]) + "m",
2573
+ "--network",
2574
+ "none",
2575
+ "--workdir",
2576
+ workdir,
2577
+ "--entrypoint",
2578
+ "bash",
2579
+ str(image["image_id"]),
2580
+ "-c",
2581
+ "sleep infinity",
2582
+ ).strip()
2583
+ docker("start", container_id)
2584
+ excluded = " -o ".join("-name " + shlex.quote(name) for name in sorted(EXCLUDED_NAMES))
2585
+ docker(
2586
+ "exec",
2587
+ container_id,
2588
+ "bash",
2589
+ "-c",
2590
+ "set -eu; find "
2591
+ + shlex.quote(workdir)
2592
+ + " -mindepth 1 \\( "
2593
+ + excluded
2594
+ + " \\) -prune -o \\( -type f -o -type l \\) -exec rm -f -- {} +; find "
2595
+ + shlex.quote(workdir)
2596
+ + " -mindepth 1 \\( "
2597
+ + excluded
2598
+ + ' \\) -prune -o -type d -print0 | sort -zr | while IFS= read -r -d \'\' directory; do if [ -z "$(ls -A -- "$directory")" ]; then rmdir -- "$directory"; fi; done',
2599
+ )
2600
+ docker("cp", str(snapshot) + "/.", container_id + ":" + workdir + "/", limit=300)
2601
+ before = export_native_source(
2602
+ destination=destination / "before",
2603
+ project=project,
2604
+ trial_dir=destination,
2605
+ workdir=workdir,
2606
+ expected=expected,
2607
+ image_id=str(image["image_id"]),
2608
+ resume=True,
2609
+ )
2610
+ if before["snapshot_digest"] != original["digest"]:
2611
+ raise ValueError("Native measurement source differs from the selected frozen snapshot")
2612
+ if _native_asset_inventory(destination / "before") != original_inventory:
2613
+ raise ValueError(
2614
+ "Native measurement source metadata differs from the selected frozen snapshot"
2615
+ )
2616
+ # A stale report must never be accepted as output of the requested command.
2617
+ target = workdir.rstrip("/") + "/" + report_path
2618
+ docker("exec", container_id, "bash", "-c", "rm -f -- " + shlex.quote(target))
2619
+ try:
2620
+ argv = [
2621
+ "docker",
2622
+ "--host",
2623
+ str(_native_object(expected["docker_engine"], "measurement engine")["endpoint"]),
2624
+ "exec",
2625
+ "--workdir",
2626
+ workdir,
2627
+ container_id,
2628
+ *command,
2629
+ ]
2630
+ cli = _object(expected["docker_cli"], "measurement Docker CLI")
2631
+ argv[0] = _frozen_executable(cli, "CLI")["resolved_path"]
2632
+ result = run_command(
2633
+ argv,
2634
+ cwd=destination,
2635
+ env={
2636
+ key: value
2637
+ for key, value in os.environ.items()
2638
+ if key not in {"DOCKER_HOST", "DOCKER_CONTEXT"}
2639
+ },
2640
+ timeout=timeout,
2641
+ )
2642
+ receipt.update(exit_code=result.returncode, stdout=result.stdout, stderr=result.stderr)
2643
+ except subprocess.TimeoutExpired as error:
2644
+ receipt.update(
2645
+ timed_out=True,
2646
+ stdout=(error.output or b"").decode(errors="replace"),
2647
+ stderr=(error.stderr or b"").decode(errors="replace"),
2648
+ )
2649
+ docker("stop", "--time", "10", container_id)
2650
+ after = export_native_source(
2651
+ destination=destination / "after",
2652
+ project=project,
2653
+ trial_dir=destination,
2654
+ workdir=workdir,
2655
+ expected=expected,
2656
+ image_id=str(image["image_id"]),
2657
+ )
2658
+ receipt["after_digest"] = after["snapshot_digest"]
2659
+ _assert_measurement_source(
2660
+ original_inventory=original_inventory,
2661
+ after=destination / "after",
2662
+ report_path=report_path,
2663
+ )
2664
+ receipt["source_integrity"] = "verified"
2665
+ if not receipt["timed_out"]:
2666
+ report = destination / "after" / relative
2667
+ # Included-source export rejects all symlinks before reaching this read.
2668
+ if report.is_file():
2669
+ shutil.copyfile(report, destination / "report.raw")
2670
+ receipt.update(
2671
+ report_collected=True,
2672
+ report_sha256=hashlib.sha256(report.read_bytes()).hexdigest(),
2673
+ )
2674
+ except BaseException as error:
2675
+ failure = error
2676
+ receipt["error"] = str(error)
2677
+ receipt["source_integrity"] = "failed"
2678
+ finally:
2679
+ try:
2680
+ receipt["cleanup"] = cleanup(project=project, owner=owner, expected_runtime=expected)
2681
+ except (OSError, ValueError, subprocess.TimeoutExpired) as error:
2682
+ receipt["cleanup"] = {"status": "incomplete", "error": str(error)}
2683
+ if failure is None:
2684
+ failure = error
2685
+ else:
2686
+ failure.add_note("Native measurement cleanup failed: " + str(error))
2687
+ if (
2688
+ inspect_snapshot(snapshot)["digest"] != original["digest"]
2689
+ or _native_asset_inventory(snapshot) != original_inventory
2690
+ ):
2691
+ failure = ValueError("Frozen measurement input changed during execution")
2692
+ codex.write_json(destination / "execution.json", receipt)
2693
+ if failure is not None:
2694
+ raise failure
2695
+ return receipt
2696
+
2697
+
2698
+ def _assert_measurement_source(
2699
+ *, original_inventory: dict[str, str], after: Path, report_path: str
2700
+ ) -> None:
2701
+ actual = _native_asset_inventory(after)
2702
+ for name, digest in original_inventory.items():
2703
+ if actual.get(name) != digest:
2704
+ raise ValueError(
2705
+ "Native measurement changed selected source bytes or metadata: " + name
2706
+ )
2707
+ parent = Path(report_path).parent
2708
+ report_tree = parent.as_posix() + "/"
2709
+ generated_tree = parent != Path(".") and not any(
2710
+ name.startswith(report_tree) and (_is_source_path(name) or _is_test_path(name))
2711
+ for name in original_inventory
2712
+ if not name.endswith("/")
2713
+ )
2714
+ for name in actual.keys() - original_inventory.keys():
2715
+ if (
2716
+ name != report_path
2717
+ and not name.endswith("/")
2718
+ and not (generated_tree and name.startswith(report_tree))
2719
+ and (_is_source_path(name) or _is_test_path(name))
2720
+ ):
2721
+ raise ValueError("Native measurement introduced production or test source: " + name)
2722
+
2723
+
2724
+ def _native_artifact_hashes(directory: Path) -> dict[str, str]:
2725
+ return {
2726
+ path.relative_to(directory).as_posix(): hashlib.sha256(path.read_bytes()).hexdigest()
2727
+ for path in directory.rglob("*")
2728
+ if path.is_file() and not path.is_symlink()
2729
+ }
2730
+
2731
+
2732
+ def _native_regrade_task(
2733
+ *, task_dir: Path, source: Path, directory: Path, artifact_hashes: dict[str, str]
2734
+ ) -> tuple[Path, dict[str, object]]:
2735
+ """Remove only redundant declarations whose bytes the convention parent captured."""
2736
+ if _native_artifact_hashes(source / "artifacts") != artifact_hashes:
2737
+ raise ValueError("Recorded native artifacts differ from their frozen hashes")
2738
+ task = importlib.import_module("harbor.models.task.task").Task(task_dir)
2739
+ helpers = importlib.import_module("harbor.models.task.artifacts")
2740
+ host_path = importlib.import_module("harbor.trial.artifact_handler").artifact_host_path
2741
+ convention = helpers.convention_source_for_os(task.config.environment.os)
2742
+ declared = helpers.normalize_artifact_entries(task.config.artifacts)
2743
+ parent = next(
2744
+ entry
2745
+ for entry in helpers.with_convention_entry(declared, convention_source=convention)
2746
+ if helpers.is_convention_entry(entry, convention)
2747
+ )
2748
+ kept = []
2749
+ removed: list[dict[str, object]] = []
2750
+ for entry in declared:
2751
+ suffix = (
2752
+ PurePosixPath(entry.source).relative_to(convention)
2753
+ if (PurePosixPath(convention) in PurePosixPath(entry.source).parents)
2754
+ else None
2755
+ )
2756
+ child = host_path(source / "artifacts", entry)
2757
+ if (
2758
+ suffix is None
2759
+ or parent.exclude
2760
+ or parent.destination is not None
2761
+ or entry.exclude
2762
+ or entry.destination is not None
2763
+ or helpers.effective_artifact_service(entry)
2764
+ != helpers.effective_artifact_service(parent)
2765
+ ):
2766
+ kept.append(entry)
2767
+ continue
2768
+ if child != host_path(source / "artifacts", parent).joinpath(*suffix.parts):
2769
+ raise ValueError("Native regrade child artifact is not at its canonical parent path")
2770
+ if not child.is_file() or child.is_symlink():
2771
+ raise ValueError(
2772
+ "Native regrade redundant child artifact is missing or not a regular file"
2773
+ )
2774
+ removed.append(
2775
+ {
2776
+ "declaration": entry.model_dump(mode="json"),
2777
+ "destination": child.relative_to(source).as_posix(),
2778
+ "sha256": artifact_hashes[child.relative_to(source / "artifacts").as_posix()],
2779
+ }
2780
+ )
2781
+ proof: dict[str, object] = {
2782
+ "original_task_assets_sha256": native_task_assets(task_dir),
2783
+ "removed_declarations": removed,
2784
+ }
2785
+ derived = task_dir
2786
+ if removed:
2787
+ proof["parent"] = _native_regrade_parent(
2788
+ source=source, parent=_native_object(parent.model_dump(mode="json"), "replay parent")
2789
+ )
2790
+ proof["manifest_sha256"] = artifact_hashes["manifest.json"]
2791
+ derived = directory / "regrade-task" / task_dir.name
2792
+ shutil.copytree(task_dir, derived)
2793
+ task.config.artifacts = kept
2794
+ (derived / "task.toml").write_text(task.config.model_dump_toml())
2795
+ proof["derived_task_assets_sha256"] = native_task_assets(derived)
2796
+ return derived, proof
2797
+
2798
+
2799
+ def _native_regrade_parent(*, source: Path, parent: dict[str, object]) -> dict[str, object]:
2800
+ helpers = importlib.import_module("harbor.models.task.artifacts")
2801
+ host_path = importlib.import_module("harbor.trial.artifact_handler").artifact_host_path
2802
+ manifest = json.loads((source / "artifacts/manifest.json").read_text())
2803
+ if not isinstance(manifest, list):
2804
+ raise ValueError("Native regrade artifact manifest must be a list")
2805
+ entry_type = importlib.import_module(
2806
+ "harbor.models.trial.artifact_manifest"
2807
+ ).ArtifactManifestEntry
2808
+ entries = [
2809
+ _native_object(
2810
+ entry_type.model_validate(entry).model_dump(mode="json"), "native artifact entry"
2811
+ )
2812
+ for entry in manifest
2813
+ ]
2814
+ artifact_type = importlib.import_module("harbor.models.task.config").ArtifactConfig
2815
+ artifact = artifact_type.model_validate(parent)
2816
+ parent_path = host_path(source / "artifacts", artifact)
2817
+ matching = [
2818
+ entry
2819
+ for entry in entries
2820
+ if (
2821
+ str(entry["source"]).rstrip("/") == artifact.source.rstrip("/")
2822
+ and (entry.get("service") or "main") == helpers.effective_artifact_service(artifact)
2823
+ and entry.get("destination") == parent_path.relative_to(source).as_posix()
2824
+ )
2825
+ ]
2826
+ if (
2827
+ len(matching) != 1
2828
+ or matching[0].get("status") != "ok"
2829
+ or matching[0].get("type") != "directory"
2830
+ or matching[0].get("exclude") != []
2831
+ or not parent_path.is_dir()
2832
+ ):
2833
+ raise ValueError("Native regrade requires one collected, unfiltered convention parent")
2834
+ return matching[0]
2835
+
2836
+
2837
+ def regrade_native(
2838
+ *,
2839
+ artifacts: Path,
2840
+ task_dir: Path,
2841
+ identity: dict[str, object],
2842
+ directory: Path,
2843
+ artifact_hashes: dict[str, str],
2844
+ ) -> dict[str, object]:
2845
+ """Replay only the standard native verifier over recorded immutable artifacts."""
2846
+ source = artifacts / "harbor/trial"
2847
+ source_digest = native_task_assets(source)
2848
+ if native_task_assets(task_dir) != identity["task_assets_sha256"]:
2849
+ raise ValueError("Native regrade task assets differ from the frozen task")
2850
+ validate_native_task(task_dir)
2851
+ # Regrading replays only the verifier, so the launcher version does not bind it.
2852
+ expected = assert_runtime(
2853
+ _native_object(identity["isolation"], "native regrade runtime"), launcher=False
2854
+ )
2855
+ grader = _native_object(identity["grader_image_identity"], "native regrade grader")
2856
+ _assert_native_image(grader, expected)
2857
+ toolchain = _native_object(identity["toolchain_identity"], "native regrade toolchain")
2858
+ directory.mkdir(parents=True, exist_ok=True)
2859
+ replay_task, normalization = _native_regrade_task(
2860
+ task_dir=task_dir, source=source, directory=directory, artifact_hashes=artifact_hashes
2861
+ )
2862
+ output = directory / "artifacts"
2863
+ output.mkdir(exist_ok=False)
2864
+ owner = uuid4().hex
2865
+ name = "pluginbench-" + owner
2866
+ config = _native_config(
2867
+ task_dir=replay_task,
2868
+ area=directory,
2869
+ owner=owner,
2870
+ harness=str(toolchain["harness"]),
2871
+ version=str(toolchain["harness_version"]),
2872
+ limits=_native_object(expected["resources"], "native regrade limits"),
2873
+ timeout=1,
2874
+ )
2875
+ config["source_trial"] = {"action": "regrade", "type": "local", "path": str(source.resolve())}
2876
+ receipt: dict[str, object] = {
2877
+ "source_sha256": source_digest,
2878
+ "task_assets_sha256": identity["task_assets_sha256"],
2879
+ "grader_image_identity": grader,
2880
+ "observer_identity": expected,
2881
+ "artifact_hashes": artifact_hashes,
2882
+ "task_normalization": normalization,
2883
+ "model_calls": 0,
2884
+ "cleanup": {"status": "incomplete"},
2885
+ }
2886
+ result: dict[str, object] = {}
2887
+ failure: BaseException | None = None
2888
+ try:
2889
+ worker = _native_process(
2890
+ area=directory,
2891
+ config=config,
2892
+ expected=expected,
2893
+ harness=str(toolchain["harness"]),
2894
+ timeout=_native_worker_budget(task_dir=task_dir),
2895
+ install_only=True,
2896
+ )
2897
+ if worker.get("session_id") != name + "__env":
2898
+ raise ValueError("Native regrade returned a foreign trial identity")
2899
+ result = _native_object(worker["result"], "native regrade result")
2900
+ if result.get("verifier_result") is not None:
2901
+ container = _native_container(
2902
+ project=name + "__verifier__trial",
2903
+ trial_dir=output / "harbor" / name,
2904
+ expected=expected,
2905
+ image_id=str(grader["image_id"]),
2906
+ )
2907
+ receipt["verifier"] = {
2908
+ "container_id": container["Id"],
2909
+ "image_id": container["Image"],
2910
+ "state": stopped_container_state(container.get("State")),
2911
+ }
2912
+ except BaseException as error:
2913
+ failure = error
2914
+ receipt["error"] = str(error)
2915
+ finally:
2916
+ receipt["cleanup"] = _native_cleanup_projects(name=name, owner=owner, expected=expected)
2917
+ if _native_object(receipt["cleanup"], "native regrade cleanup")["status"] != "complete":
2918
+ cleanup_error = ValueError("Native regrade cleanup incomplete")
2919
+ if failure is None:
2920
+ failure = cleanup_error
2921
+ else:
2922
+ failure.add_note(str(cleanup_error))
2923
+ if native_task_assets(source) != source_digest:
2924
+ failure = ValueError("Recorded native source trial changed during regrade")
2925
+ if native_task_assets(task_dir) != identity["task_assets_sha256"]:
2926
+ failure = ValueError("Frozen native task changed during regrade")
2927
+ if native_task_assets(replay_task) != normalization["derived_task_assets_sha256"]:
2928
+ failure = ValueError("Derived native replay task changed during regrade")
2929
+ codex.write_json(directory / "regrade-execution.json", receipt)
2930
+ codex.write_json(directory / "regrade-result.json", result)
2931
+ if failure is not None:
2932
+ raise failure
2933
+ return {
2934
+ "native_result": result,
2935
+ "verifier_result": result.get("verifier_result"),
2936
+ "exception_info": result.get("exception_info"),
2937
+ "receipt": receipt,
2938
+ }
2939
+
2940
+
2941
+ if __name__ == "__main__":
2942
+ # Disable .env discovery before any Harbor imports and use only the parent
2943
+ # supplied private environment, not the operator's agent settings.
2944
+ os.environ["PYTHON_DOTENV_DISABLED"] = "1"
2945
+ payload = _object(json.loads(Path(sys.argv[1]).read_text()), "native trial config")
2946
+ codex.write_json(Path(sys.argv[2]), asyncio.run(run_trial(payload)))