@tonyclaw/eval-harness 0.2.18

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.
@@ -0,0 +1,412 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import re
6
+ import shutil
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from .base import DEFAULT_AGENT_PROMPT, _command_set_named_arg
11
+ from .opencode import OpenCodeRunner
12
+
13
+ CODEX_EXECUTABLE_ENV_KEYS = (
14
+ "OEVAL_CODEX_EXECUTABLE",
15
+ "CODEX_EXECUTABLE",
16
+ "CODEX_CLI",
17
+ "CODEX_EXE",
18
+ )
19
+
20
+
21
+ class CodexCliRunner(OpenCodeRunner):
22
+ """Codex CLI agent runner adapter."""
23
+
24
+ @property
25
+ def name(self) -> str:
26
+ return "codex"
27
+
28
+ @property
29
+ def binary(self) -> str:
30
+ return "codex"
31
+
32
+ @property
33
+ def config_key(self) -> str:
34
+ return "codex"
35
+
36
+ # -- command building
37
+
38
+ def default_command(self, config: dict[str, Any]) -> list[str]:
39
+ executable = self.executable(config)
40
+ return [
41
+ executable,
42
+ "exec",
43
+ "--json",
44
+ "--skip-git-repo-check",
45
+ "--dangerously-bypass-approvals-and-sandbox",
46
+ "--cd",
47
+ "{workspace}",
48
+ DEFAULT_AGENT_PROMPT,
49
+ ]
50
+
51
+ def executable(self, config: dict[str, Any]) -> str:
52
+ for value in (
53
+ (config.get("agent") or {}).get("executable") if isinstance(config.get("agent"), dict) else None,
54
+ (config.get("codex") or {}).get("executable") if isinstance(config.get("codex"), dict) else None,
55
+ (config.get("execution") or {}).get("codex_executable") if isinstance(config.get("execution"), dict) else None,
56
+ ((config.get("execution") or {}).get("codex") or {}).get("executable")
57
+ if isinstance(config.get("execution"), dict) and isinstance((config.get("execution") or {}).get("codex"), dict)
58
+ else None,
59
+ ):
60
+ text = str(value or "").strip()
61
+ if text:
62
+ return text
63
+ return discover_codex_executable() or "codex"
64
+
65
+ def format_flags(self) -> list[str]:
66
+ return ["--json"]
67
+
68
+ def command_with_variant(self, command: list[str], variant: str) -> list[str]:
69
+ return command
70
+
71
+ def variant_cli_flag(self) -> str:
72
+ return ""
73
+
74
+ def variant_from_command(self, command: list[str]) -> str:
75
+ return ""
76
+
77
+ def default_worker_tools(self, config: dict[str, Any] | None = None) -> list[str]:
78
+ return [self.executable(config or {})]
79
+
80
+ def command_with_model(self, command: list[str], model: str) -> list[str]:
81
+ model = _codex_cli_model_name(model)
82
+ for item in command:
83
+ text = str(item)
84
+ if text == "--model" or text.startswith("--model="):
85
+ return _command_set_named_arg(command, "--model", model)
86
+ if len(command) >= 3 and str(command[1]).lower() == "exec":
87
+ value_flags = {
88
+ "--ask-for-approval",
89
+ "--cd",
90
+ "--config",
91
+ "--model",
92
+ "--sandbox",
93
+ "-C",
94
+ "-c",
95
+ "-m",
96
+ }
97
+ previous = str(command[-2]) if len(command) >= 2 else ""
98
+ last = str(command[-1])
99
+ if last and not last.startswith("-") and previous not in value_flags:
100
+ return [*command[:-1], "--model", model, command[-1]]
101
+ return [*command, "--model", model]
102
+
103
+ # -- log directory
104
+
105
+ def log_dir_name(self) -> str:
106
+ return "codex"
107
+
108
+ # -- asset installation
109
+
110
+ def default_asset_installs(self) -> list[dict[str, Any]]:
111
+ return [
112
+ {
113
+ "source": "work/work/skills/*",
114
+ "target": ".codex/skills",
115
+ "kind": "skills",
116
+ "entries": "directories",
117
+ "required": False,
118
+ "clean": True,
119
+ },
120
+ {
121
+ "source": "work/work/skills/versions.json",
122
+ "target": ".codex/skills",
123
+ "kind": "skill-manifest",
124
+ "entries": "files",
125
+ "required": False,
126
+ "clean": False,
127
+ },
128
+ {
129
+ "source": "work/skills/*",
130
+ "target": ".codex/skills",
131
+ "kind": "skills-flat-fallback",
132
+ "entries": "directories",
133
+ "required": False,
134
+ "clean": False,
135
+ },
136
+ {
137
+ "source": "work/skills/versions.json",
138
+ "target": ".codex/skills",
139
+ "kind": "skill-manifest-flat-fallback",
140
+ "entries": "files",
141
+ "required": False,
142
+ "clean": False,
143
+ },
144
+ {
145
+ "source": "work/work/skills/*.md",
146
+ "target": ".codex/agents",
147
+ "kind": "agents",
148
+ "required": False,
149
+ "clean": True,
150
+ },
151
+ {
152
+ "source": "work/skills/*.md",
153
+ "target": ".codex/agents",
154
+ "kind": "agents-flat-fallback",
155
+ "required": False,
156
+ "clean": False,
157
+ },
158
+ ]
159
+
160
+ # -- runtime profile seeding
161
+
162
+ def runtime_profile_candidates(
163
+ self, base_env: dict[str, str], runtime: dict[str, str]
164
+ ) -> list[tuple[Path | None, Path, str]]:
165
+ from ..cli import existing_path
166
+
167
+ home = Path(runtime["home_dir"])
168
+ appdata = Path(runtime.get("appdata_dir", home / "AppData" / "Roaming"))
169
+ localappdata = Path(runtime.get("localappdata_dir", home / "AppData" / "Local"))
170
+ config_dir = Path(runtime["config_dir"])
171
+ data_dir = Path(runtime["data_dir"])
172
+
173
+ return [
174
+ (existing_path(base_env.get("CODEX_HOME")), home / ".codex", "CODEX_HOME"),
175
+ (existing_path(base_env.get("USERPROFILE"), ".codex"), home / ".codex", "USERPROFILE/.codex"),
176
+ (existing_path(base_env.get("HOME"), ".codex"), home / ".codex", "HOME/.codex"),
177
+ (existing_path(base_env.get("USERPROFILE"), ".config", "codex"), config_dir / "codex", "USERPROFILE/.config/codex"),
178
+ (existing_path(base_env.get("USERPROFILE"), ".local", "share", "codex"), data_dir / "codex", "USERPROFILE/.local/share/codex"),
179
+ (existing_path(base_env.get("USERPROFILE"), ".cache", "codex"), Path(runtime["cache_dir"]) / "codex", "USERPROFILE/.cache/codex"),
180
+ (existing_path(base_env.get("APPDATA"), "codex"), appdata / "codex", "APPDATA/codex"),
181
+ (existing_path(base_env.get("LOCALAPPDATA"), "codex"), localappdata / "codex", "LOCALAPPDATA/codex"),
182
+ (existing_path(base_env.get("XDG_CONFIG_HOME"), "codex"), config_dir / "codex", "XDG_CONFIG_HOME/codex"),
183
+ (existing_path(base_env.get("XDG_DATA_HOME"), "codex"), data_dir / "codex", "XDG_DATA_HOME/codex"),
184
+ ]
185
+
186
+ # -- provider config
187
+
188
+ def write_agent_config(self, manifest: dict[str, Any]) -> str | None:
189
+ runtime = manifest.get("runtime") or {}
190
+ home = runtime.get("home_dir")
191
+ if not home:
192
+ return None
193
+ config_dir = Path(home) / ".codex"
194
+ config_dir.mkdir(parents=True, exist_ok=True)
195
+ config_path = config_dir / "config.toml"
196
+
197
+ from ..cli import execution_agent_provider # local to avoid circular import
198
+
199
+ execution = manifest.get("execution", {})
200
+ provider = execution_agent_provider(execution, self)
201
+ if not isinstance(provider, dict):
202
+ return str(config_path)
203
+ provider_id = str(provider.get("id", "agent-inspector")).strip() or "agent-inspector"
204
+ if provider_id != "agent-inspector":
205
+ return str(config_path)
206
+
207
+ base_url = str(provider.get("baseURL", "http://127.0.0.1:25947/proxy")).strip()
208
+ if not base_url:
209
+ base_url = "http://127.0.0.1:25947/proxy"
210
+ base_url = _codex_provider_base_url(base_url)
211
+ provider_name = str(provider.get("name", "Agent Inspector")).strip() or "Agent Inspector"
212
+ api_key = str(provider.get("apiKey", "") or "")
213
+ wire_api = _codex_provider_wire_api(provider)
214
+
215
+ existing = config_path.read_text(encoding="utf-8") if config_path.exists() else ""
216
+ preserved = _remove_codex_inspector_config(existing)
217
+ top_level_block = [
218
+ "# Managed by eval-harness for Agent Inspector runs.",
219
+ f'model_provider = "{_toml_escape(provider_id)}"',
220
+ ]
221
+ provider_block = [
222
+ "",
223
+ "# Managed by eval-harness for Agent Inspector runs.",
224
+ f'[model_providers."{_toml_escape(provider_id)}"]',
225
+ f'name = "{_toml_escape(provider_name)}"',
226
+ f'base_url = "{_toml_escape(base_url)}"',
227
+ f'wire_api = "{_toml_escape(wire_api)}"',
228
+ ]
229
+ if api_key:
230
+ provider_block.append(f'experimental_bearer_token = "{_toml_escape(api_key)}"')
231
+ text = _insert_codex_top_level_config(preserved, top_level_block)
232
+ text = text.rstrip() + "\n" + "\n".join(provider_block) + "\n"
233
+ config_path.write_text(text, encoding="utf-8")
234
+ return str(config_path)
235
+
236
+ # -- presentation
237
+
238
+ def extract_sessions_from_text(
239
+ self, text: str, *, inspector_base_url: str = "http://127.0.0.1:25947"
240
+ ) -> dict[str, Any]:
241
+ summary = super().extract_sessions_from_text(text, inspector_base_url=inspector_base_url)
242
+ thread_ids: list[str] = []
243
+ for raw_line in text.splitlines():
244
+ line = raw_line.strip()
245
+ if not line.startswith("{"):
246
+ continue
247
+ try:
248
+ payload = json.loads(line)
249
+ except json.JSONDecodeError:
250
+ continue
251
+ if not isinstance(payload, dict):
252
+ continue
253
+ thread_id = str(payload.get("thread_id") or "").strip()
254
+ if thread_id and thread_id not in thread_ids:
255
+ thread_ids.append(thread_id)
256
+ summary["codex_thread_ids"] = thread_ids
257
+ summary["primary_codex_thread_id"] = thread_ids[0] if thread_ids else None
258
+ return summary
259
+
260
+ def render_sessions_md(self, summary: dict[str, Any]) -> str:
261
+ text = super().render_sessions_md(summary)
262
+ text = text.replace("# Codex Sessions", "# Codex CLI Sessions", 1)
263
+ lines = [text.rstrip()]
264
+ thread_ids = summary.get("codex_thread_ids") or []
265
+ if thread_ids:
266
+ lines.extend(["", "## Codex Threads", ""])
267
+ for thread_id in thread_ids:
268
+ lines.append(f"- `{thread_id}`")
269
+ return "\n".join(lines) + "\n"
270
+
271
+
272
+ def _codex_cli_model_name(model: str) -> str:
273
+ text = str(model or "").strip()
274
+ prefix = "agent-inspector/"
275
+ if text.startswith(prefix):
276
+ text = text[len(prefix) :]
277
+ return _codex_inspector_api_model_name(text)
278
+
279
+
280
+ def discover_codex_executable(env: dict[str, str] | None = None) -> str:
281
+ """Find Codex CLI without relying on an interactive shell PATH."""
282
+ env_map = os.environ if env is None else env
283
+ for key in CODEX_EXECUTABLE_ENV_KEYS:
284
+ found = _existing_executable(env_map.get(key))
285
+ if found:
286
+ return found
287
+
288
+ if os.name == "nt":
289
+ candidates = _windows_codex_install_candidates(env_map)
290
+ if candidates:
291
+ return str(candidates[0])
292
+
293
+ for name in ("codex", "codex.exe"):
294
+ found = shutil.which(name)
295
+ if found:
296
+ return found
297
+ return ""
298
+
299
+
300
+ def _existing_executable(value: Any) -> str:
301
+ text = str(value or "").strip().strip('"')
302
+ if not text:
303
+ return ""
304
+ path = Path(text)
305
+ if path.is_file():
306
+ return str(path)
307
+ found = shutil.which(text)
308
+ return found or ""
309
+
310
+
311
+ def _windows_codex_install_candidates(env: dict[str, str]) -> list[Path]:
312
+ roots: list[Path] = []
313
+ for raw in (
314
+ env.get("LOCALAPPDATA"),
315
+ str(Path(env["USERPROFILE"]) / "AppData" / "Local") if env.get("USERPROFILE") else "",
316
+ str(Path.home() / "AppData" / "Local"),
317
+ ):
318
+ text = str(raw or "").strip()
319
+ if not text:
320
+ continue
321
+ root = Path(text) / "OpenAI" / "Codex" / "bin"
322
+ if root not in roots:
323
+ roots.append(root)
324
+
325
+ candidates: list[Path] = []
326
+ for root in roots:
327
+ if not root.exists():
328
+ continue
329
+ candidates.extend(root.glob("codex.exe"))
330
+ candidates.extend(root.glob("*/codex.exe"))
331
+
332
+ unique: dict[str, Path] = {}
333
+ for path in candidates:
334
+ if path.is_file():
335
+ unique[str(path).lower()] = path
336
+
337
+ def sort_key(path: Path) -> tuple[float, str]:
338
+ try:
339
+ mtime = path.stat().st_mtime
340
+ except OSError:
341
+ mtime = 0.0
342
+ return (mtime, str(path))
343
+
344
+ return sorted(unique.values(), key=sort_key, reverse=True)
345
+
346
+
347
+ def _codex_inspector_api_model_name(model: str) -> str:
348
+ text = str(model or "").strip()
349
+ if re.match(r"(?i)^minimax\s+m", text):
350
+ return re.sub(r"\s+", "-", text)
351
+ return text
352
+
353
+
354
+ def _remove_codex_inspector_config(text: str) -> str:
355
+ lines = text.splitlines()
356
+ result: list[str] = []
357
+ skip = False
358
+ for line in lines:
359
+ stripped = line.strip()
360
+ if stripped == "# Managed by eval-harness for Agent Inspector runs.":
361
+ continue
362
+ if stripped == 'model_provider = "agent-inspector"':
363
+ continue
364
+ if stripped == '[model_providers."agent-inspector"]' or stripped == "[model_providers.agent-inspector]":
365
+ skip = True
366
+ continue
367
+ if skip and stripped.startswith("[") and stripped.endswith("]"):
368
+ skip = False
369
+ if not skip:
370
+ result.append(line)
371
+ return "\n".join(result)
372
+
373
+
374
+ def _insert_codex_top_level_config(text: str, block: list[str]) -> str:
375
+ lines = text.rstrip().splitlines() if text.strip() else []
376
+ first_table = len(lines)
377
+ for index, line in enumerate(lines):
378
+ stripped = line.strip()
379
+ if stripped.startswith("[") and stripped.endswith("]"):
380
+ first_table = index
381
+ break
382
+
383
+ before = lines[:first_table]
384
+ after = lines[first_table:]
385
+ result: list[str] = []
386
+ result.extend(before)
387
+ if result and result[-1].strip():
388
+ result.append("")
389
+ result.extend(block)
390
+ if after:
391
+ result.append("")
392
+ result.extend(after)
393
+ return "\n".join(result)
394
+
395
+
396
+ def _codex_provider_wire_api(provider: dict[str, Any]) -> str:
397
+ for key in ("codexWireApi", "codex_wire_api", "wireApi", "wire_api"):
398
+ value = str(provider.get(key, "") or "").strip()
399
+ if value:
400
+ return value
401
+ return "responses"
402
+
403
+
404
+ def _codex_provider_base_url(base_url: str) -> str:
405
+ value = base_url.rstrip("/")
406
+ if value.endswith("/v1"):
407
+ return value
408
+ return f"{value}/v1"
409
+
410
+
411
+ def _toml_escape(value: str) -> str:
412
+ return value.replace("\\", "\\\\").replace('"', '\\"')