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.
- opal/__init__.py +7 -0
- opal/_version.py +3 -0
- opal/agent.py +394 -0
- opal/backends/__init__.py +31 -0
- opal/backends/_codex_client.py +450 -0
- opal/backends/_privacy.py +116 -0
- opal/backends/base.py +248 -0
- opal/backends/claude.py +665 -0
- opal/backends/codex.py +356 -0
- opal/backends/mailbox.py +1034 -0
- opal/backends/unavailable.py +161 -0
- opal/cli.py +396 -0
- opal/context.py +111 -0
- opal/defaults.json +11 -0
- opal/defaults.py +54 -0
- opal/estimate.py +120 -0
- opal/hf.py +109 -0
- opal/misc/__init__.py +2 -0
- opal/misc/aggressive.py +157 -0
- opal/misc/audit.py +84 -0
- opal/misc/boundary.py +226 -0
- opal/misc/checkpoint.py +399 -0
- opal/misc/ddp.py +147 -0
- opal/misc/integrity.py +263 -0
- opal/misc/nested.py +67 -0
- opal/misc/neutral_analysis.py +166 -0
- opal/misc/precision.py +210 -0
- opal/misc/recovery.py +231 -0
- opal/misc/schedulers.py +214 -0
- opal/misc/setup.py +253 -0
- opal/misc/skip.py +165 -0
- opal/misc/taskfacts.py +293 -0
- opal/misc/waiting.py +58 -0
- opal/programs.py +482 -0
- opal/prompts/example_updater.py +252 -0
- opal/prompts/neutral.md +206 -0
- opal/report.py +1001 -0
- opal/report_page.py +175 -0
- opal/report_privacy.py +159 -0
- opal/results.py +80 -0
- opal/schedule.py +134 -0
- opal/summaries.py +224 -0
- opal/wrapper.py +1674 -0
- opal_optimizer-0.3.5.dist-info/METADATA +232 -0
- opal_optimizer-0.3.5.dist-info/RECORD +49 -0
- opal_optimizer-0.3.5.dist-info/WHEEL +5 -0
- opal_optimizer-0.3.5.dist-info/entry_points.txt +2 -0
- opal_optimizer-0.3.5.dist-info/licenses/LICENSE +21 -0
- 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")
|