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,207 @@
1
+ """Git diff metrics and source/test file classification."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import fnmatch
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+
12
+ _SOURCE_SUFFIXES = {
13
+ ".c",
14
+ ".cc",
15
+ ".cjs",
16
+ ".cpp",
17
+ ".cs",
18
+ ".cts",
19
+ ".go",
20
+ ".java",
21
+ ".js",
22
+ ".jsx",
23
+ ".kt",
24
+ ".kts",
25
+ ".mjs",
26
+ ".mts",
27
+ ".php",
28
+ ".py",
29
+ ".rb",
30
+ ".rs",
31
+ ".swift",
32
+ ".ts",
33
+ ".tsx",
34
+ ".vue",
35
+ }
36
+
37
+
38
+ _DEPENDENCY_FILES = {
39
+ "cargo.lock",
40
+ "cargo.toml",
41
+ "composer.json",
42
+ "composer.lock",
43
+ "gemfile",
44
+ "gemfile.lock",
45
+ "go.mod",
46
+ "go.sum",
47
+ "gradle.lockfile",
48
+ "npm-shrinkwrap.json",
49
+ "package-lock.json",
50
+ "package.json",
51
+ "pipfile",
52
+ "pipfile.lock",
53
+ "pnpm-lock.yaml",
54
+ "poetry.lock",
55
+ "pom.xml",
56
+ "pyproject.toml",
57
+ "setup.cfg",
58
+ "setup.py",
59
+ "yarn.lock",
60
+ }
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class FileChange:
65
+ old_path: str | None
66
+ new_path: str | None
67
+ added: int
68
+ removed: int
69
+ binary: bool
70
+
71
+ @property
72
+ def path(self) -> str:
73
+ path = self.new_path or self.old_path
74
+ if path is None:
75
+ raise ValueError("diff file has no usable path")
76
+ return path
77
+
78
+
79
+ def _marker_path(value: str) -> str | None:
80
+ path = value.split("\t", 1)[0]
81
+ if path.startswith('"'):
82
+ if not path.endswith('"'):
83
+ raise ValueError(f"cannot parse diff path: {value}")
84
+ # Git's C/octal escapes represent path bytes, not Unicode code points.
85
+ decoded = path[1:-1].encode().decode("unicode_escape")
86
+ path = decoded.encode("latin-1").decode("utf-8", errors="surrogateescape")
87
+ if not path or path == "/dev/null":
88
+ return None
89
+ return path[2:] if path.startswith(("a/", "b/")) else path
90
+
91
+
92
+ def _file_changes(patch: str) -> tuple[FileChange, ...]:
93
+ changes: list[FileChange] = []
94
+ old_path: str | None = None
95
+ new_path: str | None = None
96
+ added = 0
97
+ removed = 0
98
+ binary = False
99
+ in_hunk = False
100
+ active = False
101
+
102
+ def flush() -> None:
103
+ nonlocal active, old_path, new_path, added, removed, binary, in_hunk
104
+ if active:
105
+ changes.append(FileChange(old_path, new_path, added, removed, binary))
106
+ active = False
107
+ old_path = None
108
+ new_path = None
109
+ added = 0
110
+ removed = 0
111
+ binary = False
112
+ in_hunk = False
113
+
114
+ for line in patch.splitlines():
115
+ if line.startswith("diff --git "):
116
+ flush()
117
+ header = re.fullmatch(
118
+ r'diff --git ("(?:\\.|[^"\\])*"|[ab]/.*?) ("(?:\\.|[^"\\])*"|[ab]/.*)', line
119
+ )
120
+ if header is None:
121
+ raise ValueError(f"invalid diff header: {line}")
122
+ old_path = _marker_path(header[1])
123
+ new_path = _marker_path(header[2])
124
+ active = True
125
+ elif active and not in_hunk and line.startswith("--- "):
126
+ old_path = _marker_path(line[4:])
127
+ elif active and not in_hunk and line.startswith("+++ "):
128
+ new_path = _marker_path(line[4:])
129
+ elif active and line.startswith("@@"):
130
+ in_hunk = True
131
+ elif active and (line == "GIT binary patch" or line.startswith("Binary files ")):
132
+ binary = True
133
+ in_hunk = False
134
+ elif active and in_hunk and line.startswith("+"):
135
+ added += 1
136
+ elif active and in_hunk and line.startswith("-"):
137
+ removed += 1
138
+ flush()
139
+ if patch.strip() and not changes:
140
+ raise ValueError("expected a Git diff with 'diff --git' headers; unsupported patch format")
141
+ return tuple(changes)
142
+
143
+
144
+ TEST_DIRECTORY_NAMES = frozenset(
145
+ {"test", "tests", "testing", "spec", "specs", "__tests", "__tests__"}
146
+ )
147
+ TEST_FILENAME_PATTERNS = ("test_*", "*.test.*", "*.spec.*", "*_test.py", "*_test.go")
148
+ TEST_PATH_PATTERNS = tuple(
149
+ pattern
150
+ for directory in sorted(TEST_DIRECTORY_NAMES)
151
+ for pattern in (f"{directory}/**", f"**/{directory}/**")
152
+ ) + tuple(
153
+ pattern for filename in TEST_FILENAME_PATTERNS for pattern in (filename, f"**/{filename}")
154
+ )
155
+
156
+
157
+ def _is_test_path(path: str) -> bool:
158
+ parts = Path(path).parts
159
+ name = Path(path).name.lower()
160
+ return any(part.lower() in TEST_DIRECTORY_NAMES for part in parts[:-1]) or any(
161
+ fnmatch.fnmatchcase(name, pattern) for pattern in TEST_FILENAME_PATTERNS
162
+ )
163
+
164
+
165
+ def _is_source_path(path: str) -> bool:
166
+ return not _is_test_path(path) and Path(path).suffix.lower() in _SOURCE_SUFFIXES
167
+
168
+
169
+ def _is_dependency_manifest(path: str) -> bool:
170
+ name = Path(path).name.lower()
171
+ return (
172
+ name in _DEPENDENCY_FILES
173
+ or (name.startswith("requirements") and name.endswith(".txt"))
174
+ or name.startswith("build.gradle")
175
+ )
176
+
177
+
178
+ def analyze_patch(patch: str) -> dict[str, Any]:
179
+ """Return transparent line and path metrics for one saved Git patch."""
180
+ changes = _file_changes(patch)
181
+ source = [change for change in changes if _is_source_path(change.path)]
182
+ tests = [change for change in changes if _is_test_path(change.path)]
183
+ manifests = sorted(change.path for change in changes if _is_dependency_manifest(change.path))
184
+ lines_added = sum(change.added for change in changes)
185
+ lines_removed = sum(change.removed for change in changes)
186
+ source_added = sum(change.added for change in source)
187
+ source_removed = sum(change.removed for change in source)
188
+ test_added = sum(change.added for change in tests)
189
+ test_removed = sum(change.removed for change in tests)
190
+ return {
191
+ "changed_paths": sorted(change.path for change in changes),
192
+ "files_changed": len(changes),
193
+ "source_files_changed": len(source),
194
+ "test_files_changed": len(tests),
195
+ "lines_added": lines_added,
196
+ "lines_removed": lines_removed,
197
+ "churn": lines_added + lines_removed,
198
+ "source_lines_added": source_added,
199
+ "source_lines_removed": source_removed,
200
+ "source_churn": source_added + source_removed,
201
+ "test_lines_added": test_added,
202
+ "test_lines_removed": test_removed,
203
+ "test_churn": test_added + test_removed,
204
+ "binary_patch": any(change.binary for change in changes),
205
+ "empty_patch": not patch.strip(),
206
+ "dependency_manifest_changes": manifests,
207
+ }
pluginbench/codex.py ADDED
@@ -0,0 +1,451 @@
1
+ """Native CLI execution and evidence for isolated reviews and Harbor submissions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import shutil
8
+ import stat
9
+ import subprocess
10
+ import tarfile
11
+ import time
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from pluginbench import isolation
16
+ from pluginbench.codex_events import admit_version as admit_version
17
+ from pluginbench.codex_events import observed_cli_version as observed_cli_version
18
+ from pluginbench.codex_events import parse_events as parse_events
19
+ from pluginbench.processes import terminate_process_group
20
+
21
+
22
+ GUIDANCE_NAMES = {
23
+ "AGENTS.md",
24
+ "AGENTS.override.md",
25
+ "CLAUDE.md",
26
+ ".agents",
27
+ ".claude",
28
+ ".codex",
29
+ ".claude-plugin",
30
+ }
31
+
32
+
33
+ def reject_guidance(
34
+ workspace: Path, *, include_ancestors: bool = True, reject_git: bool = False
35
+ ) -> None:
36
+ """Reject undeclared instruction discovery surfaces in task source."""
37
+ root = workspace.resolve()
38
+ if not root.is_dir():
39
+ raise ValueError(f"workspace is not a directory: {workspace}")
40
+ contaminants = []
41
+ for path in root.rglob("*"):
42
+ relative = path.relative_to(root)
43
+ if ".git" in relative.parts and not reject_git:
44
+ continue
45
+ if path.is_symlink():
46
+ contaminants.append(str(path))
47
+ elif path.name in GUIDANCE_NAMES or (reject_git and path.name == ".git"):
48
+ contaminants.append(str(path))
49
+ if include_ancestors:
50
+ contaminants.extend(
51
+ str(parent / name)
52
+ for parent in root.parents
53
+ for name in GUIDANCE_NAMES
54
+ if (parent / name).exists()
55
+ )
56
+ if contaminants:
57
+ raise ValueError(
58
+ "custom guidance or unsupported fixture paths: " + ", ".join(sorted(contaminants))
59
+ )
60
+
61
+
62
+ def preflight() -> dict[str, Any]:
63
+ """Check native runtime capabilities locally without invoking a model."""
64
+ outputs = {}
65
+ for key, arguments in (("version", ["--version"]), ("help", ["exec", "--help"])):
66
+ result = subprocess.run(
67
+ ["codex", *arguments], capture_output=True, text=True, timeout=15, check=False
68
+ )
69
+ if result.returncode:
70
+ raise ValueError(f"codex {key} check failed (exit {result.returncode})")
71
+ outputs[key] = result.stdout
72
+ required = (
73
+ "--ignore-user-config",
74
+ "--ignore-rules",
75
+ "--json",
76
+ "--model",
77
+ "--config",
78
+ "--dangerously-bypass-approvals-and-sandbox",
79
+ "--skip-git-repo-check",
80
+ "--cd",
81
+ )
82
+ missing = [flag for flag in required if flag not in outputs["help"]]
83
+ if missing:
84
+ raise ValueError("native codex lacks required isolation flags: " + ", ".join(missing))
85
+ return {
86
+ "harness": "codex-cli",
87
+ "harness_version": outputs["version"].strip(),
88
+ "help": outputs["help"],
89
+ "required_flags": list(required),
90
+ "paid_calls": 0,
91
+ }
92
+
93
+
94
+ def write_json(path: Path, value: object) -> None:
95
+ path.parent.mkdir(parents=True, exist_ok=True)
96
+ temporary = path.with_suffix(path.suffix + ".new")
97
+ temporary.write_text(json.dumps(value, indent=2) + "\n")
98
+ temporary.replace(path)
99
+
100
+
101
+ def credential_values(auth_file: Path) -> tuple[str, ...]:
102
+ """Collect credential scalars for redaction without exposing them in receipts."""
103
+ values: list[str] = []
104
+
105
+ def collect(value: Any) -> None:
106
+ if isinstance(value, dict):
107
+ for child in value.values():
108
+ collect(child)
109
+ elif isinstance(value, list):
110
+ for child in value:
111
+ collect(child)
112
+ elif isinstance(value, str) and len(value) > 20:
113
+ values.append(value)
114
+
115
+ collect(json.loads(auth_file.read_text()))
116
+ return tuple(values)
117
+
118
+
119
+ def validate_artifacts(directory: Path) -> None:
120
+ """Accept only ordinary directories and files before reading native evidence."""
121
+ if directory.is_symlink() or not directory.is_dir():
122
+ raise ValueError("native artifact root must be an ordinary directory")
123
+
124
+ def failed(error: OSError) -> None:
125
+ raise error
126
+
127
+ for root, names, files in os.walk(directory, followlinks=False, onerror=failed):
128
+ for name in [*names, *files]:
129
+ path = Path(root) / name
130
+ mode = path.lstat().st_mode
131
+ if not (stat.S_ISREG(mode) or stat.S_ISDIR(mode)):
132
+ raise ValueError(f"unsupported native artifact: {path.relative_to(directory)}")
133
+
134
+
135
+ def copy_artifacts(source: Path, target: Path) -> str | None:
136
+ """Preserve unsupported evidence as an inert archive, without following links."""
137
+ try:
138
+ validate_artifacts(source)
139
+ except (OSError, ValueError) as error:
140
+ target.mkdir(parents=True)
141
+ with tarfile.open(target / "unsupported-artifacts.tar", "w", dereference=False) as archive:
142
+ archive.add(source, arcname="artifacts")
143
+ write_json(target / "unsupported-artifacts.json", {"error": str(error)})
144
+ return str(error)
145
+ shutil.copytree(source, target, symlinks=True)
146
+ return None
147
+
148
+
149
+ def scrub_artifacts(directory: Path, secrets: tuple[str, ...]) -> None:
150
+ if directory.is_symlink() or not directory.is_dir():
151
+ return
152
+ for root, names, files in os.walk(directory, followlinks=False):
153
+ names[:] = [name for name in names if not (Path(root) / name).is_symlink()]
154
+ for name in files:
155
+ path = Path(root) / name
156
+ if not stat.S_ISREG(path.lstat().st_mode):
157
+ continue
158
+ original = path.read_bytes()
159
+ content = original
160
+ for secret in secrets:
161
+ content = content.replace(secret.encode(), b"<redacted-credential>")
162
+ if content != original:
163
+ path.write_bytes(content)
164
+
165
+
166
+ def _native_traces(private: Path, artifacts: Path, secrets: tuple[str, ...]) -> list[str]:
167
+ sessions = private / "codex/sessions"
168
+ for path in (private, private / "codex", sessions):
169
+ if path.is_symlink():
170
+ copy_artifacts(path, artifacts / "unsupported-native-traces")
171
+ raise ValueError("unsupported native session ancestor link")
172
+ if path.exists() and not path.is_dir():
173
+ raise ValueError("native session ancestors must be ordinary directories")
174
+ if not sessions.exists():
175
+ return []
176
+ try:
177
+ validate_artifacts(sessions)
178
+ except (OSError, ValueError):
179
+ scrub_artifacts(sessions, secrets)
180
+ copy_artifacts(sessions, artifacts / "unsupported-native-traces")
181
+ raise
182
+ return [path.read_text() for path in sorted(sessions.rglob("*.jsonl"))]
183
+
184
+
185
+ def runtime_environment(
186
+ private: Path, host: str, *, auth_file: Path | None = None
187
+ ) -> tuple[dict[str, str], tuple[str, ...]]:
188
+ if host == "claude":
189
+ from pluginbench import claude
190
+
191
+ return claude.runtime_environment(private, auth_file=auth_file)
192
+ private.mkdir(mode=0o700)
193
+ home = private / "home"
194
+ home.mkdir(mode=0o700)
195
+ temporary = private / "tmp"
196
+ temporary.mkdir(mode=0o700)
197
+ env = {
198
+ key: value
199
+ for key, value in os.environ.items()
200
+ if key
201
+ in {
202
+ "PATH",
203
+ "LANG",
204
+ "LC_ALL",
205
+ "SSL_CERT_FILE",
206
+ "SSL_CERT_DIR",
207
+ "NODE_EXTRA_CA_CERTS",
208
+ "HTTP_PROXY",
209
+ "HTTPS_PROXY",
210
+ "NO_PROXY",
211
+ }
212
+ }
213
+ env.update(
214
+ HOME=str(home), TMPDIR=str(temporary), XDG_CONFIG_HOME=str(home / ".config"), NO_COLOR="1"
215
+ )
216
+ secrets: list[str] = []
217
+ if host == "codex":
218
+ config = private / "codex"
219
+ config.mkdir(mode=0o700)
220
+ auth = auth_file if auth_file is not None else Path.home() / ".codex/auth.json"
221
+ shutil.copy2(auth, config / "auth.json")
222
+ (config / "auth.json").chmod(0o600)
223
+
224
+ secrets.extend(credential_values(auth))
225
+ env["CODEX_HOME"] = str(config)
226
+ else:
227
+ raise ValueError(f"unsupported native host: {host}")
228
+ return env, tuple(secrets)
229
+
230
+
231
+ def command(
232
+ *,
233
+ host: str,
234
+ model: str,
235
+ workspace: Path,
236
+ effort: str = "medium",
237
+ ) -> list[str]:
238
+ if host == "claude":
239
+ from pluginbench import claude
240
+
241
+ return claude.command(model=model, effort=effort)
242
+ if host == "codex":
243
+ return [
244
+ "codex",
245
+ "exec",
246
+ "--ignore-user-config",
247
+ "--ignore-rules",
248
+ "--json",
249
+ "-m",
250
+ model,
251
+ "-c",
252
+ f"model_reasoning_effort={json.dumps(effort)}",
253
+ "-c",
254
+ 'web_search="disabled"',
255
+ "-c",
256
+ "features.multi_agent=false",
257
+ "--enable",
258
+ "code_mode",
259
+ "--enable",
260
+ "code_mode_only",
261
+ # The mandatory outer OS wrapper owns read-only scope. Nested Seatbelt fails.
262
+ "--dangerously-bypass-approvals-and-sandbox",
263
+ "--skip-git-repo-check",
264
+ "-C",
265
+ str(workspace),
266
+ "-",
267
+ ]
268
+ raise ValueError(f"unsupported native host: {host}")
269
+
270
+
271
+ def validated_claude_launch(execution: dict[str, Any], model: str, effort: str) -> bool:
272
+ """Bind a requested effort to an isolated launch without claiming effective effort."""
273
+ launch = execution.get("launch")
274
+ if not isinstance(launch, dict) or execution.get("actual_primary_models") != [model]:
275
+ return False
276
+ argv = launch.get("argv")
277
+ return (
278
+ bool(
279
+ launch.get("harness") == "claude-cli"
280
+ and launch.get("requested_model") == model
281
+ and launch.get("requested_effort") == effort
282
+ and launch.get("harness_version")
283
+ and isinstance(argv, list)
284
+ and ["--model", model] == argv[argv.index("--model") : argv.index("--model") + 2]
285
+ and ["--effort", effort] == argv[argv.index("--effort") : argv.index("--effort") + 2]
286
+ and "--safe-mode" in argv
287
+ and execution.get("runtime_effort_evidence") in ([], [effort])
288
+ )
289
+ if isinstance(argv, list) and "--model" in argv and "--effort" in argv
290
+ else False
291
+ )
292
+
293
+
294
+ def invoke(
295
+ *,
296
+ area: Path,
297
+ entry: dict[str, Any],
298
+ protocol: dict[str, Any],
299
+ prompt: str,
300
+ timeout: int,
301
+ output_schema: Path | None = None,
302
+ ) -> dict[str, Any]:
303
+ """Invoke a local reviewer with an OS-enforced read-only source packet."""
304
+ artifacts = area / "artifacts"
305
+ artifacts.mkdir(exist_ok=True)
306
+ if any((artifacts / name).exists() for name in ("invocation.json", "execution.json")):
307
+ raise ValueError("native invocation artifacts already exist; use a fresh trial area")
308
+ private = area / "private"
309
+ effort = protocol.get("reasoning", "medium")
310
+ if not isinstance(effort, str) or not effort:
311
+ raise ValueError("reasoning must be a nonempty effort string")
312
+ argv = command(
313
+ host=entry["host"],
314
+ model=protocol["models"][entry["host"]],
315
+ workspace=area / "workspace",
316
+ effort=effort,
317
+ )
318
+ if output_schema is not None:
319
+ if entry["host"] == "claude":
320
+ argv.extend(["--json-schema", json.dumps(json.loads(output_schema.read_text()))])
321
+ else:
322
+ argv[-1:-1] = ["--output-schema", str(output_schema)]
323
+ launch = {
324
+ "harness": entry["host"] + "-cli",
325
+ "requested_model": protocol["models"][entry["host"]],
326
+ "requested_effort": effort,
327
+ "argv": argv,
328
+ }
329
+ if entry["host"] == "claude":
330
+ from pluginbench import claude
331
+
332
+ launch["harness_version"] = claude.preflight()["harness_version"]
333
+ write_json(
334
+ artifacts / "invocation.json",
335
+ {**launch, "timeout_seconds": timeout},
336
+ )
337
+ (artifacts / "prompt.txt").write_text(prompt)
338
+ secrets: tuple[str, ...] = ()
339
+ started = time.monotonic()
340
+ timed_out = False
341
+ proc = None
342
+ traces = []
343
+ failure: BaseException | None = None
344
+ try:
345
+ auth_file = protocol.get("auth_file")
346
+ env, secrets = runtime_environment(
347
+ private,
348
+ entry["host"],
349
+ **({"auth_file": Path(auth_file).expanduser()} if auth_file is not None else {}),
350
+ )
351
+ argv, filesystem = isolation.command(
352
+ argv,
353
+ writable=[private],
354
+ readable=[area / "workspace", *([output_schema] if output_schema else [])],
355
+ expected=protocol.get("isolation"),
356
+ )
357
+ write_json(
358
+ artifacts / "runtime-isolation.json",
359
+ {
360
+ "environment_keys": sorted(env),
361
+ "private_startup_files": sorted(
362
+ str(p.relative_to(private)) for p in private.rglob("*") if p.is_file()
363
+ ),
364
+ "fresh_home": not any((private / "home").iterdir())
365
+ if (private / "home").is_dir()
366
+ else None,
367
+ "configuration_source": "private auth only; user/project setting sources disabled",
368
+ "native_builtin_instructions": "retained",
369
+ "filesystem": filesystem,
370
+ },
371
+ )
372
+ with (
373
+ (artifacts / "stdout.jsonl").open("w") as stdout,
374
+ (artifacts / "stderr.log").open("w") as stderr,
375
+ ):
376
+ proc = subprocess.Popen(
377
+ argv,
378
+ cwd=area / "workspace",
379
+ env=env,
380
+ stdin=subprocess.PIPE,
381
+ stdout=stdout,
382
+ stderr=stderr,
383
+ text=True,
384
+ start_new_session=True,
385
+ )
386
+ try:
387
+ proc.communicate(prompt, timeout=timeout)
388
+ except subprocess.TimeoutExpired:
389
+ timed_out = True
390
+ except BaseException as error:
391
+ failure = error
392
+ raise
393
+ finally:
394
+ cleanup_error: OSError | subprocess.TimeoutExpired | None = None
395
+ trace_error: OSError | ValueError | None = None
396
+ try:
397
+ if proc is not None:
398
+ terminate_process_group(proc)
399
+ except (OSError, subprocess.TimeoutExpired) as error:
400
+ cleanup_error = error
401
+ if failure is not None:
402
+ failure.add_note(f"Process-group cleanup failed: {error}")
403
+ try:
404
+ traces = _native_traces(private, artifacts, secrets)
405
+ for index, trace in enumerate(traces):
406
+ (artifacts / f"host-trace-{index}.jsonl").write_text(trace)
407
+ except (OSError, ValueError) as error:
408
+ trace_error = error
409
+ if failure is not None:
410
+ failure.add_note(f"Native session evidence rejected: {error}")
411
+ finally:
412
+ try:
413
+ if private.is_symlink():
414
+ private.unlink()
415
+ elif private.exists():
416
+ shutil.rmtree(private)
417
+ finally:
418
+ try:
419
+ scrub_artifacts(artifacts, secrets)
420
+ finally:
421
+ write_json(
422
+ artifacts / "execution.json",
423
+ {
424
+ "exit_code": proc.returncode if proc is not None else None,
425
+ "timed_out": timed_out,
426
+ "elapsed_seconds": time.monotonic() - started,
427
+ "private_runtime_removed": not private.exists(),
428
+ "cleanup_error": str(cleanup_error)
429
+ if cleanup_error is not None
430
+ else None,
431
+ "trace_error": str(trace_error) if trace_error is not None else None,
432
+ "successful_native_completion": False
433
+ if trace_error is not None
434
+ else not timed_out and proc is not None and proc.returncode == 0,
435
+ },
436
+ )
437
+ if cleanup_error is not None and failure is None:
438
+ raise cleanup_error
439
+ if trace_error is not None and failure is None:
440
+ raise trace_error
441
+ validate_artifacts(artifacts)
442
+ traces = [p.read_text() for p in sorted(artifacts.glob("host-trace-*.jsonl"))]
443
+ parsed = parse_events(
444
+ (artifacts / "stdout.jsonl").read_text(), traces, entry["host"], requested_effort=effort
445
+ )
446
+ if timed_out or proc is None or proc.returncode != 0:
447
+ parsed["successful_native_completion"] = False
448
+ parsed["launch"] = launch
449
+ parsed["launch_harness_version"] = launch.get("harness_version")
450
+ write_json(artifacts / "host-evidence.json", parsed)
451
+ return {**json.loads((artifacts / "execution.json").read_text()), **parsed}