opal-optimizer 0.3.5__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 (49) hide show
  1. opal/__init__.py +7 -0
  2. opal/_version.py +3 -0
  3. opal/agent.py +394 -0
  4. opal/backends/__init__.py +31 -0
  5. opal/backends/_codex_client.py +450 -0
  6. opal/backends/_privacy.py +116 -0
  7. opal/backends/base.py +248 -0
  8. opal/backends/claude.py +665 -0
  9. opal/backends/codex.py +356 -0
  10. opal/backends/mailbox.py +1034 -0
  11. opal/backends/unavailable.py +161 -0
  12. opal/cli.py +396 -0
  13. opal/context.py +111 -0
  14. opal/defaults.json +11 -0
  15. opal/defaults.py +54 -0
  16. opal/estimate.py +120 -0
  17. opal/hf.py +109 -0
  18. opal/misc/__init__.py +2 -0
  19. opal/misc/aggressive.py +157 -0
  20. opal/misc/audit.py +84 -0
  21. opal/misc/boundary.py +226 -0
  22. opal/misc/checkpoint.py +399 -0
  23. opal/misc/ddp.py +147 -0
  24. opal/misc/integrity.py +263 -0
  25. opal/misc/nested.py +67 -0
  26. opal/misc/neutral_analysis.py +166 -0
  27. opal/misc/precision.py +210 -0
  28. opal/misc/recovery.py +231 -0
  29. opal/misc/schedulers.py +214 -0
  30. opal/misc/setup.py +253 -0
  31. opal/misc/skip.py +165 -0
  32. opal/misc/taskfacts.py +293 -0
  33. opal/misc/waiting.py +58 -0
  34. opal/programs.py +482 -0
  35. opal/prompts/example_updater.py +252 -0
  36. opal/prompts/neutral.md +206 -0
  37. opal/report.py +1001 -0
  38. opal/report_page.py +175 -0
  39. opal/report_privacy.py +159 -0
  40. opal/results.py +80 -0
  41. opal/schedule.py +134 -0
  42. opal/summaries.py +224 -0
  43. opal/wrapper.py +1674 -0
  44. opal_optimizer-0.3.5.dist-info/METADATA +232 -0
  45. opal_optimizer-0.3.5.dist-info/RECORD +49 -0
  46. opal_optimizer-0.3.5.dist-info/WHEEL +5 -0
  47. opal_optimizer-0.3.5.dist-info/entry_points.txt +2 -0
  48. opal_optimizer-0.3.5.dist-info/licenses/LICENSE +21 -0
  49. opal_optimizer-0.3.5.dist-info/top_level.txt +1 -0
opal/backends/codex.py ADDED
@@ -0,0 +1,356 @@
1
+ """OPAL Sol: isolated Codex app-server, one durable thread per run.
2
+
3
+ Only OPAL's new message is sent on each turn. The subprocess transport is short lived; thread/resume loads the
4
+ provider's durable conversation. See _codex_client.py for protocol handling and the fail-closed rollout audit.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ import platform
12
+ import shutil
13
+ import subprocess
14
+ import sys
15
+ import time
16
+ from pathlib import Path
17
+
18
+ from .._version import __version__
19
+ from ._privacy import redact_secrets
20
+
21
+ from .base import Cancelled, agent_env
22
+ from .claude import ClaudeBackend, NO_GPU, SANDBOX_NOTE, _contains, _python_paths
23
+
24
+ DEFAULT_MODEL, DEFAULT_EFFORT = "gpt-6.1-sol", "high"
25
+ TOKEN_FIELDS = ("input_tokens", "cached_input_tokens", "output_tokens", "reasoning_output_tokens")
26
+
27
+
28
+ def isolated_home(value=None):
29
+ """Never borrow the coding agent's home or login, including via a symlink/hardlink."""
30
+ home = Path(value).expanduser() if value is not None else Path.home() / ".codex-opal"
31
+ default = (Path.home() / ".codex").resolve()
32
+ home = home.resolve()
33
+ if home == default or _contains(default, home) or _contains(home, default):
34
+ raise RuntimeError("opal: Codex requires its own CODEX_HOME, outside ~/.codex; use ~/.codex-opal")
35
+ auth = home / "auth.json"
36
+ if auth.is_symlink():
37
+ raise RuntimeError("opal: isolated Codex auth.json must not be a symlink; log in separately")
38
+ original = default / "auth.json"
39
+ if auth.exists() and original.exists() and os.path.samefile(auth, original):
40
+ raise RuntimeError("opal: do not link/copy the default Codex login; log in separately in the isolated home")
41
+ return home
42
+
43
+
44
+ class CodexBackend(ClaudeBackend):
45
+ """Reuse Claude's retry/wait deadline policy, without changing the Claude backend or its defaults.
46
+
47
+ max_tokens is a soft run cap on input + output tokens (cached input and reasoning are subsets). A turn may
48
+ exceed it; no subsequent inference starts. Dollar budgets cannot be enforced by this backend.
49
+ """
50
+
51
+ name = "codex"
52
+
53
+ def __init__(
54
+ self,
55
+ model=DEFAULT_MODEL,
56
+ effort=DEFAULT_EFFORT,
57
+ binary="codex",
58
+ codex_home=None,
59
+ timeout=None,
60
+ retries=None,
61
+ backoff=15.0,
62
+ backoff_max=600.0,
63
+ unavailable_timeout=None,
64
+ max_tokens=None,
65
+ ):
66
+ if effort not in ("high", "xhigh"):
67
+ raise ValueError("opal: Codex effort must be 'high' or 'xhigh'")
68
+ if max_tokens is not None and (isinstance(max_tokens, bool) or int(max_tokens) != max_tokens or max_tokens < 1):
69
+ raise ValueError("opal: Codex max_tokens must be a positive integer")
70
+ super().__init__(
71
+ model=model,
72
+ effort=effort,
73
+ binary=binary,
74
+ sandbox=False,
75
+ timeout=timeout,
76
+ retries=retries,
77
+ backoff=backoff,
78
+ backoff_max=backoff_max,
79
+ unavailable_timeout=unavailable_timeout,
80
+ )
81
+ self.codex_home = isolated_home(codex_home)
82
+ self.max_tokens = max_tokens
83
+ self.tokens = dict.fromkeys(TOKEN_FIELDS, 0)
84
+ self.provider_total = {}
85
+ self.rollout = None
86
+ self.audit_offset = 0
87
+ self.poisoned = None
88
+ self.sandbox = True
89
+
90
+ def preflight(self):
91
+ isolated_home(self.codex_home)
92
+ if not shutil.which(self.binary):
93
+ raise RuntimeError(f"opal: Codex CLI ({self.binary!r}) not found; install Codex and log in in its own home")
94
+ if platform.system() not in ("Darwin", "Linux"):
95
+ raise RuntimeError("opal: Codex backend requires the macOS or Linux OS sandbox")
96
+ p = subprocess.run(
97
+ [self.binary, "--version"],
98
+ capture_output=True,
99
+ text=True,
100
+ timeout=30,
101
+ env=agent_env(extra={"CODEX_HOME": str(self.codex_home)}),
102
+ )
103
+ if p.returncode:
104
+ raise RuntimeError("opal: Codex CLI could not start: " + redact_secrets(p.stderr)[-300:])
105
+ self.version = p.stdout.strip()
106
+
107
+ def login_status(self):
108
+ """No inference and no credential content returned. Missing login is waited for by decide()."""
109
+ isolated_home(self.codex_home)
110
+ p = subprocess.run(
111
+ [self.binary, "login", "status"],
112
+ capture_output=True,
113
+ text=True,
114
+ timeout=30,
115
+ env=agent_env(extra={"CODEX_HOME": str(self.codex_home)}),
116
+ )
117
+ return p.returncode == 0
118
+
119
+ def reproduction_note(self):
120
+ return (
121
+ f"OPAL Sol uses {self.model} at effort {self.effort}; Claude remains the default backend. "
122
+ "Codex reports tokens, not USD; max_cost_usd does not cap this backend."
123
+ )
124
+
125
+ def setup(self, run_dir, workspace):
126
+ self.run_dir, self.workspace = Path(run_dir).resolve(), Path(workspace).resolve()
127
+ self.control = self.run_dir / "control"
128
+ self.control.mkdir(mode=0o700, parents=True, exist_ok=True)
129
+ self.flags = self._config()
130
+ (self.control / "codex-config.json").write_text(json.dumps(self.flags, indent=1))
131
+
132
+ def _config(self):
133
+ py, dropped = _python_paths(protect=(str(self.workspace), str(self.run_dir)))
134
+ fs = {":root": "deny", ":minimal": "read", ":tmpdir": "deny", ":slash_tmp": "deny"}
135
+ fs.update({p: "read" for p in py})
136
+ # macOS must be able to resolve a symlinked Python installation's directory entry. Codex canonicalizes
137
+ # permission paths; grant that installation parent, while denying its other children explicitly.
138
+ for p in py:
139
+ if Path(p).is_symlink() and Path(p).is_dir():
140
+ parent = Path(p).parent
141
+ if any(_contains(parent, x) for x in (self.workspace, self.run_dir, Path.home(), self.codex_home)):
142
+ continue
143
+ fs[str(parent)] = "read"
144
+ allowed = {Path(x).resolve() for x in py}
145
+ for child in parent.iterdir():
146
+ if child.resolve() not in allowed:
147
+ fs[str(child)] = "deny"
148
+ fs[str(self.workspace)] = "write"
149
+ fs[str(self.control)] = "deny"
150
+ fs[str(self.codex_home)] = "deny"
151
+ shell = agent_env(extra=NO_GPU)
152
+ shell["PATH"] = os.pathsep.join(
153
+ dict.fromkeys([str(Path(sys.executable).parent), "/usr/bin", "/bin", "/usr/sbin", "/sbin"])
154
+ )
155
+ shell["TMPDIR"] = str(self.workspace / "scratch")
156
+ flags = {
157
+ "approval_policy": "never",
158
+ "default_permissions": "opal",
159
+ "permissions.opal.extends": ":workspace",
160
+ "permissions.opal.filesystem": fs,
161
+ "permissions.opal.network.enabled": False,
162
+ "model_provider": "openai",
163
+ "model": self.model,
164
+ "model_reasoning_effort": self.effort,
165
+ "include_environment_context": False,
166
+ "include_permissions_instructions": False,
167
+ "include_apps_instructions": False,
168
+ "include_collaboration_mode_instructions": False,
169
+ "project_doc_max_bytes": 0,
170
+ "skills.include_instructions": False,
171
+ "web_search": "disabled",
172
+ "tools.update_plan.enabled": False,
173
+ "shell_environment_policy.inherit": "none",
174
+ "shell_environment_policy.set": shell,
175
+ }
176
+ for feature in (
177
+ "apps",
178
+ "plugins",
179
+ "hooks",
180
+ "memories",
181
+ "multi_agent",
182
+ "multi_agent_v2",
183
+ "shell_snapshot",
184
+ "shell_snapshot_v2",
185
+ "browser_use",
186
+ "computer_use",
187
+ "image_generation",
188
+ "view_image",
189
+ "goals",
190
+ "tool_suggest",
191
+ "current_time_reminder",
192
+ "request_permissions_tool",
193
+ "sleep_tool",
194
+ "standalone_web_search",
195
+ "unbounded_connection_retries",
196
+ "code_mode",
197
+ "in_app_browser",
198
+ "skill_search",
199
+ "workspace_dependencies",
200
+ ):
201
+ flags[f"features.{feature}"] = False
202
+ # The model catalog uses code_mode_only: exec coordinates sandboxed tools in an OS/network-free V8 isolate.
203
+ flags["features.code_mode_host"] = True
204
+ # Preserve model metadata, except its generic multi-agent instruction selector. No account data is copied.
205
+ cache = self.codex_home / "models_cache.json"
206
+ if cache.exists():
207
+ catalog = json.loads(cache.read_text())
208
+ selected = next((m for m in catalog.get("models", []) if m.get("slug") == self.model), None)
209
+ if selected:
210
+ target = self.control / "codex-model-catalog.json"
211
+ target.write_text(json.dumps({"models": [dict(selected, multi_agent_version=None)]}, indent=1))
212
+ flags["model_catalog_json"] = str(target)
213
+ if dropped:
214
+ self._say("Python read paths containing the run or home were excluded: " + ", ".join(dropped))
215
+ return flags
216
+
217
+ def _budget(self):
218
+ return None # Codex gives no dollar cost; never pretend the caller's USD cap is enforced.
219
+
220
+ def _say(self, text):
221
+ text = text.replace("Claude Code", "Codex CLI").replace(
222
+ "run `claude`, then /login", "run CODEX_HOME=~/.codex-opal codex login --device-auth"
223
+ )
224
+ super()._say(text)
225
+
226
+ def decide(self, workspace, message, system, record_dir):
227
+ self._decision_usage = dict.fromkeys(TOKEN_FIELDS, 0)
228
+ self._usage_reported = False
229
+ # Match the exact OPAL-specific text ClaudeBackend._cmd appends, including its existing shell note.
230
+ # Do not edit the Claude note just to adapt its provider-specific bookkeeping sentence to Codex.
231
+ backend_system = system + "\n\n" + SANDBOX_NOTE if system else ""
232
+ (self.control / "codex-system.md").write_text(backend_system)
233
+ out = super().decide(workspace, message, backend_system, record_dir)
234
+ out.update(
235
+ usage=dict(self._decision_usage) if self._usage_reported else None,
236
+ session=self.session,
237
+ rollout=self.rollout,
238
+ )
239
+ return out
240
+
241
+ def _call(self, workspace, message, system, record_dir, timeout):
242
+ t0 = time.time()
243
+ if self.poisoned:
244
+ return dict(text="", cost_usd=None, returncode=-1, error=self.poisoned, seconds=0, final=True)
245
+ if (
246
+ self.max_tokens is not None
247
+ and self.tokens["input_tokens"] + self.tokens["output_tokens"] >= self.max_tokens
248
+ ):
249
+ return dict(
250
+ text="",
251
+ cost_usd=None,
252
+ returncode=-1,
253
+ error="Codex max_tokens reached (soft run cap)",
254
+ seconds=0,
255
+ final=True,
256
+ )
257
+ if self.session_cwd and self.session_cwd != str(Path(workspace).resolve()):
258
+ return dict(
259
+ text="", cost_usd=None, returncode=-1, error="Codex thread workspace changed", seconds=0, final=True
260
+ )
261
+ payload = dict(
262
+ client_version=__version__,
263
+ binary=self.binary,
264
+ home=str(isolated_home(self.codex_home)),
265
+ flags=self.flags,
266
+ workspace=str(Path(workspace).resolve()),
267
+ model=self.model,
268
+ effort=self.effort,
269
+ stderr=str(self.control / "codex-stderr.txt"),
270
+ session=self.session,
271
+ system=system or "",
272
+ message=message,
273
+ rollout=self.rollout,
274
+ audit_offset=self.audit_offset,
275
+ provider_total=self.provider_total,
276
+ timeout=max(0.1, timeout - 4),
277
+ )
278
+ transcript = Path(record_dir) / "backend.jsonl"
279
+ failed = None
280
+ with transcript.open("w") as output:
281
+ try:
282
+ p = self._run(
283
+ [sys.executable, str(Path(__file__).with_name("_codex_client.py"))],
284
+ input=json.dumps(payload),
285
+ stdout=output,
286
+ cwd=workspace,
287
+ timeout=timeout,
288
+ env=agent_env(extra={"CODEX_HOME": str(self.codex_home), **NO_GPU}),
289
+ )
290
+ if p.returncode:
291
+ failed = f"Codex transport exited {p.returncode}: {p.stderr[-500:]}"
292
+ except subprocess.TimeoutExpired:
293
+ failed = f"timed out after {timeout:.0f} s"
294
+ except Cancelled as exc:
295
+ failed = str(exc)
296
+ result = None
297
+ for line in transcript.read_text().splitlines():
298
+ try:
299
+ event = json.loads(line)
300
+ except ValueError:
301
+ continue
302
+ if event.get("type") == "opal_thread":
303
+ self.session = event["session"]
304
+ self.session_cwd = payload["workspace"]
305
+ self.rollout = event.get("rollout")
306
+ self.started = True
307
+ if event.get("type") == "opal_result":
308
+ result = event["result"]
309
+ if result is None:
310
+ # A killed turn may have appended context; never replay an observation into uncertain history.
311
+ self.poisoned = failed or "Codex transport ended without a result; thread history is uncertain"
312
+ return dict(
313
+ text="", cost_usd=None, returncode=-1, error=self.poisoned, seconds=time.time() - t0, final=True
314
+ )
315
+ self.audit_offset = result.pop("audit_offset", self.audit_offset)
316
+ self.provider_total = result.pop("provider_total", self.provider_total)
317
+ self._usage_reported = self._usage_reported or bool(result.get("usage"))
318
+ for k in TOKEN_FIELDS:
319
+ n = result.get("usage", {}).get(k, 0)
320
+ self.tokens[k] += n
321
+ self._decision_usage[k] += n
322
+ audit = result.pop("audit", None)
323
+ if audit:
324
+ (Path(record_dir) / "codex-audit.json").write_text(json.dumps(audit, indent=1))
325
+ if result.pop("poisoned", False):
326
+ self.poisoned = result.get("error") or "Codex isolation audit failed"
327
+ result["final"] = True
328
+ result.pop("unavailable", None)
329
+ return dict(result, cost_usd=None, seconds=time.time() - t0)
330
+
331
+ def _worked(self, transcript, workspace, start):
332
+ # The client reverts failed turns before a retry, and records whether any tool actually ran.
333
+ return '"worked": true' in Path(transcript).read_text() or super()._worked(transcript, workspace, start)
334
+
335
+ def state(self):
336
+ return dict(
337
+ session=self.session,
338
+ started=self.started,
339
+ cwd=self.session_cwd,
340
+ rollout=self.rollout,
341
+ audit_offset=self.audit_offset,
342
+ provider_total=self.provider_total,
343
+ tokens=dict(self.tokens),
344
+ poisoned=self.poisoned,
345
+ )
346
+
347
+ def restore(self, state):
348
+ self.session, self.started, self.session_cwd = (
349
+ state.get("session"),
350
+ state.get("started", False),
351
+ state.get("cwd"),
352
+ )
353
+ self.rollout, self.audit_offset = state.get("rollout"), state.get("audit_offset", 0)
354
+ self.provider_total = state.get("provider_total", {})
355
+ self.tokens = dict(state.get("tokens") or dict.fromkeys(TOKEN_FIELDS, 0))
356
+ self.poisoned = state.get("poisoned")