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
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
"""One isolated app-server connection. Executed as a child, so Backend owns its whole process group.
|
|
2
|
+
|
|
3
|
+
No OPAL imports: this file also runs when Python's module search path is restricted. Stdout contains JSONL records,
|
|
4
|
+
never login/account contents. A successful turn is accepted only after auditing the provider's actual rollout.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import queue
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
import tempfile
|
|
15
|
+
import threading
|
|
16
|
+
import time
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
if __package__:
|
|
20
|
+
from ._privacy import redact_record, redact_stream
|
|
21
|
+
else: # standalone transport: importing the sibling helper does not import OPAL or torch
|
|
22
|
+
from _privacy import redact_record, redact_stream
|
|
23
|
+
|
|
24
|
+
FIELDS = {
|
|
25
|
+
"inputTokens": "input_tokens",
|
|
26
|
+
"cachedInputTokens": "cached_input_tokens",
|
|
27
|
+
"outputTokens": "output_tokens",
|
|
28
|
+
"reasoningOutputTokens": "reasoning_output_tokens",
|
|
29
|
+
}
|
|
30
|
+
TOOLS = {"exec_command", "write_stdin", "shell_command", "apply_patch", "exec", "wait"}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def emit(value):
|
|
34
|
+
print(json.dumps(redact_record(value)), flush=True)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def toml(value):
|
|
38
|
+
if isinstance(value, dict):
|
|
39
|
+
return "{" + ", ".join(json.dumps(k) + " = " + toml(v) for k, v in value.items()) + "}"
|
|
40
|
+
if isinstance(value, list):
|
|
41
|
+
return "[" + ", ".join(toml(v) for v in value) + "]"
|
|
42
|
+
return json.dumps(value)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def classify(error):
|
|
46
|
+
"""Codex structured errors supplement the common Claude-oriented text classifier, without changing it."""
|
|
47
|
+
text = str(error)
|
|
48
|
+
low = text.lower()
|
|
49
|
+
for kind, words in (
|
|
50
|
+
("login", ("unauthorized", "not logged in", "authentication", "401", "invalid_api_key")),
|
|
51
|
+
("limit", ("usagelimitexceeded", "ratelimitexceeded", "usage limit", "rate limit", "429", "quota")),
|
|
52
|
+
(
|
|
53
|
+
"down",
|
|
54
|
+
(
|
|
55
|
+
"serveroverloaded",
|
|
56
|
+
"httpconnectionfailed",
|
|
57
|
+
"responsestream",
|
|
58
|
+
"connection",
|
|
59
|
+
"network",
|
|
60
|
+
"fetch failed",
|
|
61
|
+
"500",
|
|
62
|
+
"502",
|
|
63
|
+
"503",
|
|
64
|
+
"504",
|
|
65
|
+
"overloaded",
|
|
66
|
+
),
|
|
67
|
+
),
|
|
68
|
+
("missing", ("no such file", "not found")),
|
|
69
|
+
):
|
|
70
|
+
if any(w in low for w in words):
|
|
71
|
+
return [kind, text[:300]]
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class IsolationError(RuntimeError):
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class Client:
|
|
80
|
+
def __init__(self, request):
|
|
81
|
+
self.request = request
|
|
82
|
+
self.n = 0
|
|
83
|
+
self.events = queue.Queue(maxsize=4096)
|
|
84
|
+
self.usage = None
|
|
85
|
+
self.thread = None
|
|
86
|
+
self.turn = None
|
|
87
|
+
self.pending_turn = False
|
|
88
|
+
self.messages = []
|
|
89
|
+
self.worked = False
|
|
90
|
+
cmd = [request["binary"], "app-server", "--stdio"]
|
|
91
|
+
for key, value in request["flags"].items():
|
|
92
|
+
cmd += ["-c", key + "=" + toml(value)]
|
|
93
|
+
# stderr is separate from the wire transcript; it may name account paths, but never read auth contents.
|
|
94
|
+
self.stderr = tempfile.TemporaryFile(mode="w+")
|
|
95
|
+
self.proc = subprocess.Popen(
|
|
96
|
+
cmd,
|
|
97
|
+
stdin=subprocess.PIPE,
|
|
98
|
+
stdout=subprocess.PIPE,
|
|
99
|
+
stderr=self.stderr,
|
|
100
|
+
text=True,
|
|
101
|
+
bufsize=1,
|
|
102
|
+
cwd=request["workspace"],
|
|
103
|
+
env=dict(os.environ, CODEX_HOME=request["home"]),
|
|
104
|
+
)
|
|
105
|
+
self.deadline = time.monotonic() + request.get("timeout", 3600)
|
|
106
|
+
threading.Thread(target=self._reader, daemon=True).start()
|
|
107
|
+
|
|
108
|
+
def _reader(self):
|
|
109
|
+
try:
|
|
110
|
+
for line in self.proc.stdout:
|
|
111
|
+
self.events.put(json.loads(line))
|
|
112
|
+
except Exception as exc:
|
|
113
|
+
self.events.put(exc)
|
|
114
|
+
finally:
|
|
115
|
+
self.events.put(None)
|
|
116
|
+
|
|
117
|
+
def send(self, message):
|
|
118
|
+
self.proc.stdin.write(json.dumps(message) + "\n")
|
|
119
|
+
self.proc.stdin.flush()
|
|
120
|
+
|
|
121
|
+
def next(self):
|
|
122
|
+
remaining = self.deadline - time.monotonic()
|
|
123
|
+
if remaining <= 0:
|
|
124
|
+
raise TimeoutError("Codex decision timed out")
|
|
125
|
+
try:
|
|
126
|
+
event = self.events.get(timeout=remaining)
|
|
127
|
+
except queue.Empty as exc:
|
|
128
|
+
raise TimeoutError("Codex decision timed out") from exc
|
|
129
|
+
if event is None or isinstance(event, Exception):
|
|
130
|
+
raise RuntimeError("Codex app-server exited or broke the protocol")
|
|
131
|
+
method = event.get("method", "")
|
|
132
|
+
if method.startswith("account/") or method == "authRecovery":
|
|
133
|
+
return event # do not retain account identities or credentials
|
|
134
|
+
emit(dict(type="rpc", event=event))
|
|
135
|
+
if "method" in event and "id" in event:
|
|
136
|
+
self.send(
|
|
137
|
+
dict(id=event["id"], error=dict(code=-32601, message="OPAL never grants approvals or external tools"))
|
|
138
|
+
)
|
|
139
|
+
raise IsolationError("Unexpected Codex approval/tool request: " + method)
|
|
140
|
+
params = event.get("params", {})
|
|
141
|
+
if method == "thread/tokenUsage/updated":
|
|
142
|
+
if params.get("threadId") == self.thread and (self.turn is None or params.get("turnId") == self.turn):
|
|
143
|
+
self.usage = params.get("tokenUsage")
|
|
144
|
+
if method == "item/completed":
|
|
145
|
+
item = params.get("item", {})
|
|
146
|
+
if item.get("type") == "agentMessage":
|
|
147
|
+
self.messages.append(item.get("text", ""))
|
|
148
|
+
elif item.get("type") in ("commandExecution", "fileChange"):
|
|
149
|
+
self.worked = True
|
|
150
|
+
return event
|
|
151
|
+
|
|
152
|
+
def rpc(self, method, params, private=False):
|
|
153
|
+
self.n += 1
|
|
154
|
+
rid = self.n
|
|
155
|
+
self.send(dict(id=rid, method=method, params=params))
|
|
156
|
+
while True:
|
|
157
|
+
if private:
|
|
158
|
+
# Account/config responses are not logged; notifications are consumed normally thereafter.
|
|
159
|
+
remaining = self.deadline - time.monotonic()
|
|
160
|
+
if remaining <= 0:
|
|
161
|
+
raise TimeoutError("Codex decision timed out")
|
|
162
|
+
try:
|
|
163
|
+
event = self.events.get(timeout=remaining)
|
|
164
|
+
except queue.Empty as exc:
|
|
165
|
+
raise TimeoutError("Codex decision timed out") from exc
|
|
166
|
+
if event is None or isinstance(event, Exception):
|
|
167
|
+
raise RuntimeError("Codex app-server exited")
|
|
168
|
+
if event.get("id") != rid:
|
|
169
|
+
continue
|
|
170
|
+
else:
|
|
171
|
+
event = self.next()
|
|
172
|
+
if event.get("id") != rid:
|
|
173
|
+
continue
|
|
174
|
+
if "error" in event:
|
|
175
|
+
raise RuntimeError(method + ": " + json.dumps(event["error"]))
|
|
176
|
+
return event["result"]
|
|
177
|
+
|
|
178
|
+
def start(self):
|
|
179
|
+
r = self.request
|
|
180
|
+
init = self.rpc(
|
|
181
|
+
"initialize",
|
|
182
|
+
dict(clientInfo=dict(name="opal_sol", version=r["client_version"]), capabilities=dict(experimentalApi=True)),
|
|
183
|
+
)
|
|
184
|
+
if Path(init["codexHome"]).resolve() != Path(r["home"]).resolve():
|
|
185
|
+
raise IsolationError("App-server used a different CODEX_HOME")
|
|
186
|
+
self.send(dict(method="initialized"))
|
|
187
|
+
account = self.rpc("account/read", dict(refreshToken=False), private=True)
|
|
188
|
+
if not account.get("account"):
|
|
189
|
+
raise RuntimeError("Not logged in: run CODEX_HOME=~/.codex-opal codex login --device-auth")
|
|
190
|
+
cfg = self.rpc("config/read", dict(includeLayers=False), private=True).get("config", {})
|
|
191
|
+
config = {"mcp_servers": {name: {"enabled": False} for name in cfg.get("mcp_servers", {})}}
|
|
192
|
+
params = dict(
|
|
193
|
+
model=r["model"],
|
|
194
|
+
modelProvider=r["flags"]["model_provider"],
|
|
195
|
+
cwd=r["workspace"],
|
|
196
|
+
approvalPolicy="never",
|
|
197
|
+
permissions="opal",
|
|
198
|
+
runtimeWorkspaceRoots=[r["workspace"]],
|
|
199
|
+
baseInstructions=r["system"],
|
|
200
|
+
developerInstructions="",
|
|
201
|
+
config=config,
|
|
202
|
+
)
|
|
203
|
+
if r.get("session"):
|
|
204
|
+
params.update(threadId=r["session"], excludeTurns=True)
|
|
205
|
+
out = self.rpc("thread/resume", params)
|
|
206
|
+
else:
|
|
207
|
+
params.update(ephemeral=False, historyMode="paginated", allowProviderModelFallback=False)
|
|
208
|
+
out = self.rpc("thread/start", params)
|
|
209
|
+
self.thread = out["thread"]["id"]
|
|
210
|
+
self.rollout = out["thread"].get("path")
|
|
211
|
+
emit(dict(type="opal_thread", session=self.thread, rollout=self.rollout))
|
|
212
|
+
if r.get("session") and self.thread != r["session"]:
|
|
213
|
+
raise IsolationError("Codex resumed a different thread")
|
|
214
|
+
if out.get("model") != r["model"] or out.get("reasoningEffort") != r["effort"]:
|
|
215
|
+
raise IsolationError("Codex model/effort differs from requested model/effort")
|
|
216
|
+
if out.get("instructionSources"):
|
|
217
|
+
raise IsolationError("Codex loaded instruction sources: " + str(out["instructionSources"]))
|
|
218
|
+
if out.get("approvalPolicy") != "never":
|
|
219
|
+
raise IsolationError("Codex approval policy changed")
|
|
220
|
+
sandbox = out.get("sandbox", {})
|
|
221
|
+
if (
|
|
222
|
+
sandbox.get("type") != "workspaceWrite"
|
|
223
|
+
or sandbox.get("networkAccess") is not False
|
|
224
|
+
or sandbox.get("excludeTmpdirEnvVar") is not True
|
|
225
|
+
or sandbox.get("excludeSlashTmp") is not True
|
|
226
|
+
):
|
|
227
|
+
raise IsolationError("Codex workspace-write/no-network/no-global-temp policy changed")
|
|
228
|
+
roots = out.get("runtimeWorkspaceRoots", [])
|
|
229
|
+
if [str(Path(x).resolve()) for x in roots] != [str(Path(r["workspace"]).resolve())]:
|
|
230
|
+
raise IsolationError("Codex runtime workspace roots changed")
|
|
231
|
+
if out.get("activePermissionProfile", {}).get("id") != "opal":
|
|
232
|
+
raise IsolationError("Codex restricted filesystem profile changed")
|
|
233
|
+
if not self.rollout:
|
|
234
|
+
raise IsolationError("Provider rollout path unavailable; cannot audit context")
|
|
235
|
+
return out
|
|
236
|
+
|
|
237
|
+
def run_turn(self):
|
|
238
|
+
r = self.request
|
|
239
|
+
self.pending_turn = True
|
|
240
|
+
turn = self.rpc(
|
|
241
|
+
"turn/start",
|
|
242
|
+
dict(
|
|
243
|
+
threadId=self.thread,
|
|
244
|
+
input=[dict(type="text", text=r["message"])],
|
|
245
|
+
model=r["model"],
|
|
246
|
+
effort=r["effort"],
|
|
247
|
+
approvalPolicy="never",
|
|
248
|
+
permissions="opal",
|
|
249
|
+
cwd=r["workspace"],
|
|
250
|
+
runtimeWorkspaceRoots=[r["workspace"]],
|
|
251
|
+
),
|
|
252
|
+
)
|
|
253
|
+
self.turn = turn["turn"]["id"]
|
|
254
|
+
while True:
|
|
255
|
+
event = self.next()
|
|
256
|
+
if event.get("method") == "turn/completed":
|
|
257
|
+
params = event["params"]
|
|
258
|
+
if params.get("threadId") == self.thread and params["turn"]["id"] == self.turn:
|
|
259
|
+
return params["turn"]
|
|
260
|
+
|
|
261
|
+
def audit(self, successful):
|
|
262
|
+
"""Incremental provider audit, including hidden calls and all message roles. Never scan the whole home."""
|
|
263
|
+
r = self.request
|
|
264
|
+
path = Path(self.rollout)
|
|
265
|
+
offset = r.get("audit_offset", 0)
|
|
266
|
+
users, replies, kinds, tools, initial, seen_turn = [], [], [], [], bool(offset), False
|
|
267
|
+
calls = set()
|
|
268
|
+
# Turn completion can precede the rollout writer's flush. Bounded drain; each byte read only once.
|
|
269
|
+
deadline = time.monotonic() + 5
|
|
270
|
+
while time.monotonic() < deadline:
|
|
271
|
+
if path.exists():
|
|
272
|
+
with path.open() as stream:
|
|
273
|
+
stream.seek(offset)
|
|
274
|
+
while True:
|
|
275
|
+
before = stream.tell()
|
|
276
|
+
line = stream.readline()
|
|
277
|
+
if not line or not line.endswith("\n"):
|
|
278
|
+
stream.seek(before)
|
|
279
|
+
break
|
|
280
|
+
obj = json.loads(line)
|
|
281
|
+
p = obj.get("payload", {})
|
|
282
|
+
kind = obj.get("type")
|
|
283
|
+
if kind == "session_meta":
|
|
284
|
+
if p.get("base_instructions", {}).get("text") != r["system"]:
|
|
285
|
+
raise IsolationError("Provider baseInstructions differs from OPAL system text")
|
|
286
|
+
initial = True
|
|
287
|
+
elif kind == "turn_context":
|
|
288
|
+
if p.get("model") != r["model"] or p.get("effort") != r["effort"]:
|
|
289
|
+
raise IsolationError("Provider rollout model/effort mismatch")
|
|
290
|
+
if p.get("developer_instructions"):
|
|
291
|
+
raise IsolationError("Unexpected provider developer instructions")
|
|
292
|
+
seen_turn = True
|
|
293
|
+
elif kind == "compacted":
|
|
294
|
+
raise IsolationError(
|
|
295
|
+
"Codex compacted persistent history; full conversation no longer preserved"
|
|
296
|
+
)
|
|
297
|
+
elif kind == "world_state":
|
|
298
|
+
state = p.get("state", {})
|
|
299
|
+
if state.get("agents_md") or state.get("managed_developer_instructions"):
|
|
300
|
+
raise IsolationError("Provider world_state includes external instructions")
|
|
301
|
+
for key in ("skills", "host_skills"):
|
|
302
|
+
if state.get(key, {}).get("includeInstructions"):
|
|
303
|
+
raise IsolationError("Provider world_state enables skills instructions")
|
|
304
|
+
if any(
|
|
305
|
+
state.get(k)
|
|
306
|
+
for k in ("apps_instructions", "plugins_instructions", "environments_instructions")
|
|
307
|
+
):
|
|
308
|
+
raise IsolationError(
|
|
309
|
+
"Provider world_state includes app/plugin/environment instructions"
|
|
310
|
+
)
|
|
311
|
+
elif kind == "response_item":
|
|
312
|
+
item = p.get("type")
|
|
313
|
+
kinds.append(item)
|
|
314
|
+
if item == "message":
|
|
315
|
+
role = p.get("role")
|
|
316
|
+
text = "".join(c.get("text", "") for c in p.get("content", []))
|
|
317
|
+
if role == "user":
|
|
318
|
+
users.append(text)
|
|
319
|
+
elif role == "assistant":
|
|
320
|
+
replies.append(text)
|
|
321
|
+
elif role != "assistant":
|
|
322
|
+
raise IsolationError("Unexpected provider message role: " + str(role))
|
|
323
|
+
elif item == "reasoning":
|
|
324
|
+
pass
|
|
325
|
+
elif item in ("function_call", "custom_tool_call"):
|
|
326
|
+
name = p.get("name", "").removeprefix("functions.")
|
|
327
|
+
if name not in TOOLS:
|
|
328
|
+
raise IsolationError("Unexpected provider tool: " + name)
|
|
329
|
+
calls.add(p.get("call_id"))
|
|
330
|
+
tools.append(dict(name=name, call_id=p.get("call_id")))
|
|
331
|
+
elif item in ("function_call_output", "custom_tool_call_output"):
|
|
332
|
+
if p.get("call_id") not in calls:
|
|
333
|
+
raise IsolationError("Unmatched provider tool output")
|
|
334
|
+
else:
|
|
335
|
+
raise IsolationError("Unexpected provider non-message item: " + str(item))
|
|
336
|
+
offset = stream.tell()
|
|
337
|
+
final_seen = bool(self.messages) and self.messages[-1] in replies
|
|
338
|
+
if initial and seen_turn and users and (not successful or final_seen):
|
|
339
|
+
break
|
|
340
|
+
time.sleep(0.1)
|
|
341
|
+
if not initial or not seen_turn or users != [r["message"]] or (successful and not final_seen):
|
|
342
|
+
raise IsolationError("Provider context audit incomplete or user content differs from the new OPAL message")
|
|
343
|
+
total = (self.usage or {}).get("total")
|
|
344
|
+
previous = r.get("provider_total", {})
|
|
345
|
+
if successful and total is None:
|
|
346
|
+
raise IsolationError("Codex cumulative token usage missing")
|
|
347
|
+
usage = {}
|
|
348
|
+
if total is not None:
|
|
349
|
+
for field, label in FIELDS.items():
|
|
350
|
+
delta = total[field] - previous.get(field, 0)
|
|
351
|
+
if delta < 0:
|
|
352
|
+
raise IsolationError("Codex cumulative token count decreased")
|
|
353
|
+
usage[label] = delta
|
|
354
|
+
return dict(
|
|
355
|
+
passed=True,
|
|
356
|
+
thread=self.thread,
|
|
357
|
+
turn=self.turn,
|
|
358
|
+
rollout=str(path),
|
|
359
|
+
initial_base_matches=True,
|
|
360
|
+
new_user_message_matches=True,
|
|
361
|
+
response_item_types=kinds,
|
|
362
|
+
tools=tools,
|
|
363
|
+
usage=usage,
|
|
364
|
+
provider_total=total or previous,
|
|
365
|
+
audit_offset=offset,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
def revert(self):
|
|
369
|
+
"""Discard an unavailable/failed turn before retry; the durable thread id stays the same."""
|
|
370
|
+
self.deadline = time.monotonic() + 10
|
|
371
|
+
out = self.rpc("thread/revert", dict(threadId=self.thread, beforeTurnId=self.turn))
|
|
372
|
+
if out.get("thread", {}).get("id") != self.thread:
|
|
373
|
+
raise IsolationError("Codex failed-turn rollback changed the conversation id")
|
|
374
|
+
self.rollout = out["thread"].get("path") or self.rollout
|
|
375
|
+
emit(dict(type="opal_thread", session=self.thread, rollout=self.rollout))
|
|
376
|
+
return Path(self.rollout).stat().st_size
|
|
377
|
+
|
|
378
|
+
def close(self):
|
|
379
|
+
if self.proc.poll() is None:
|
|
380
|
+
self.proc.terminate()
|
|
381
|
+
try:
|
|
382
|
+
self.proc.wait(timeout=3)
|
|
383
|
+
except subprocess.TimeoutExpired:
|
|
384
|
+
self.proc.kill()
|
|
385
|
+
self.proc.wait()
|
|
386
|
+
self.stderr.seek(0)
|
|
387
|
+
with Path(self.request["stderr"]).open("w") as output:
|
|
388
|
+
redact_stream(self.stderr, output)
|
|
389
|
+
self.stderr.close()
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def main():
|
|
393
|
+
request = json.load(sys.stdin)
|
|
394
|
+
client = None
|
|
395
|
+
result = dict(text="", returncode=-1, error=None)
|
|
396
|
+
try:
|
|
397
|
+
client = Client(request)
|
|
398
|
+
client.start()
|
|
399
|
+
turn = client.run_turn()
|
|
400
|
+
success = turn["status"] == "completed"
|
|
401
|
+
audit = client.audit(success)
|
|
402
|
+
result.update(
|
|
403
|
+
text="\n".join(client.messages),
|
|
404
|
+
usage=audit["usage"],
|
|
405
|
+
provider_total=audit["provider_total"],
|
|
406
|
+
audit_offset=audit["audit_offset"],
|
|
407
|
+
audit=audit,
|
|
408
|
+
worked=client.worked,
|
|
409
|
+
)
|
|
410
|
+
if success:
|
|
411
|
+
result.update(returncode=0)
|
|
412
|
+
else:
|
|
413
|
+
error = json.dumps(turn.get("error") or turn["status"])
|
|
414
|
+
result["error"] = error
|
|
415
|
+
down = classify(error)
|
|
416
|
+
if down:
|
|
417
|
+
result["unavailable"] = down
|
|
418
|
+
result["audit_offset"] = client.revert()
|
|
419
|
+
except Exception as exc:
|
|
420
|
+
result.update(error=str(exc), returncode=-1)
|
|
421
|
+
if isinstance(exc, IsolationError):
|
|
422
|
+
result.update(poisoned=True)
|
|
423
|
+
else:
|
|
424
|
+
down = classify(str(exc))
|
|
425
|
+
if down:
|
|
426
|
+
result["unavailable"] = down
|
|
427
|
+
if client is not None and client.turn is not None:
|
|
428
|
+
try:
|
|
429
|
+
client.deadline = time.monotonic() + 10
|
|
430
|
+
client.rpc("turn/interrupt", dict(threadId=client.thread, turnId=client.turn))
|
|
431
|
+
result["audit_offset"] = client.revert()
|
|
432
|
+
except Exception:
|
|
433
|
+
result.update(poisoned=True)
|
|
434
|
+
elif client is not None and client.pending_turn:
|
|
435
|
+
result.update(poisoned=True)
|
|
436
|
+
finally:
|
|
437
|
+
if client is not None:
|
|
438
|
+
# Audit rejection does not erase measured consumption. A missing report stays unknown, not zero.
|
|
439
|
+
total = (client.usage or {}).get("total")
|
|
440
|
+
if total is not None and not result.get("usage"):
|
|
441
|
+
previous = request.get("provider_total", {})
|
|
442
|
+
delta = {label: total[field] - previous.get(field, 0) for field, label in FIELDS.items()}
|
|
443
|
+
if all(n >= 0 for n in delta.values()):
|
|
444
|
+
result.update(usage=delta, provider_total=total)
|
|
445
|
+
client.close()
|
|
446
|
+
emit(dict(type="opal_result", result=result))
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
if __name__ == "__main__":
|
|
450
|
+
main()
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Credential-shaped text is masked before diagnostics become OPAL records."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# Prefixes are case sensitive and must start a token, not a suffix of a model or function name.
|
|
8
|
+
_SECRET = re.compile(
|
|
9
|
+
r"(?<![A-Za-z0-9_])(?:sk-ant-|sk-|ghp_|github_pat_)[A-Za-z0-9_\-]{16,}|"
|
|
10
|
+
r"(?<![A-Za-z0-9_])hf_[A-Za-z0-9]{16,}(?![A-Za-z0-9_])|"
|
|
11
|
+
r"(?<![A-Za-z0-9_])AKIA[A-Z0-9]{16}(?![A-Za-z0-9_])|"
|
|
12
|
+
r"(?i:\bBearer)\s+[^\s\"'<>\\,;{}\[\]&]+|"
|
|
13
|
+
r"(?i:(?<=\baccess_token=))[^\s&\"'<>]+",
|
|
14
|
+
)
|
|
15
|
+
_ESCAPE = re.compile(r'\\(?:u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8}|x[0-9a-fA-F]{2}|[\\/"\'abfnrtv])|%[0-9a-fA-F]{2}')
|
|
16
|
+
_STRING_JOIN = re.compile(r"""(?:\"{3}|'{3}|(?<!')'(?!'')|(?<!\")\"(?!\"\"))\s*(?:[bBuUrR]{0,2})(?:\"{3}|'{3}|'(?!')|\"(?!\"))""")
|
|
17
|
+
_ESCAPE = re.compile(_ESCAPE.pattern + "|" + _STRING_JOIN.pattern + r"|(?i:(?<=Bearer))\+")
|
|
18
|
+
_SIMPLE = {'a': '\a', 'b': '\b', 'f': '\f', 'n': '\n', 'r': '\r', 't': '\t', 'v': '\v'}
|
|
19
|
+
_DECODE_DEPTH = 8
|
|
20
|
+
_DECODE_LENGTH = 65_536
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _decoded_spans(text):
|
|
24
|
+
"""Find secrets in bounded decoded views and map them back to spans in the original text.
|
|
25
|
+
|
|
26
|
+
JSON strings and exception reprs can add several layers of backslashes. Decode escapes only for
|
|
27
|
+
matching, never rewrite the diagnostic itself. Windows bound both the index map and decode work;
|
|
28
|
+
overlap retains credentials straddling a window. Direct matching also covers arbitrarily long keys.
|
|
29
|
+
"""
|
|
30
|
+
spans = [(m.start(), m.end()) for m in _SECRET.finditer(text)]
|
|
31
|
+
overlap = 4096
|
|
32
|
+
for offset in range(0, len(text), _DECODE_LENGTH - overlap):
|
|
33
|
+
decoded = text[offset:offset + _DECODE_LENGTH]
|
|
34
|
+
if '\\' not in decoded and '%' not in decoded and '+' not in decoded and not _STRING_JOIN.search(decoded):
|
|
35
|
+
continue
|
|
36
|
+
positions = [(offset + i, offset + i + 1) for i in range(len(decoded))]
|
|
37
|
+
for _ in range(_DECODE_DEPTH):
|
|
38
|
+
pieces, mapped, end = [], [], 0
|
|
39
|
+
for match in _ESCAPE.finditer(decoded):
|
|
40
|
+
pieces.append(decoded[end:match.start()])
|
|
41
|
+
mapped.extend(positions[end:match.start()])
|
|
42
|
+
escape = match[0]
|
|
43
|
+
if escape[0] in "\"'":
|
|
44
|
+
char = "" # adjacent Python string literals, as printed in diagnostics/reprs
|
|
45
|
+
elif escape == "+":
|
|
46
|
+
char = " " # application/x-www-form-urlencoded bearer separator
|
|
47
|
+
elif escape[0] == '%':
|
|
48
|
+
char = chr(int(escape[1:], 16))
|
|
49
|
+
elif escape[1] in 'uUx':
|
|
50
|
+
value = int(escape[2:], 16)
|
|
51
|
+
char = chr(value) if value <= 0x10ffff else escape
|
|
52
|
+
else:
|
|
53
|
+
char = _SIMPLE.get(escape[1], escape[1])
|
|
54
|
+
pieces.append(char)
|
|
55
|
+
mapped.extend([(positions[match.start()][0], positions[match.end() - 1][1])] * len(char))
|
|
56
|
+
end = match.end()
|
|
57
|
+
if end == 0:
|
|
58
|
+
break
|
|
59
|
+
pieces.append(decoded[end:])
|
|
60
|
+
mapped.extend(positions[end:])
|
|
61
|
+
decoded, positions = ''.join(pieces), mapped
|
|
62
|
+
for match in _SECRET.finditer(decoded):
|
|
63
|
+
start, stop = positions[match.start()]
|
|
64
|
+
for left, right in positions[match.start() + 1:match.end()]:
|
|
65
|
+
if left <= stop:
|
|
66
|
+
stop = max(stop, right)
|
|
67
|
+
else:
|
|
68
|
+
# Keep the quotes between adjacent literals, masking each credential fragment.
|
|
69
|
+
spans.append((start, stop))
|
|
70
|
+
start, stop = left, right
|
|
71
|
+
spans.append((start, stop))
|
|
72
|
+
return spans
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def redact_secrets(text):
|
|
76
|
+
"""One filter for records, subsequent prompts and HTML; preserve all noncredential source spans."""
|
|
77
|
+
text = str(text or "")
|
|
78
|
+
spans = sorted(_decoded_spans(text))
|
|
79
|
+
pieces, end = [], 0
|
|
80
|
+
for start, stop in spans:
|
|
81
|
+
if start > end:
|
|
82
|
+
pieces.append(text[end:start])
|
|
83
|
+
if stop > end:
|
|
84
|
+
if start >= end:
|
|
85
|
+
pieces.append('<hidden>')
|
|
86
|
+
end = stop
|
|
87
|
+
pieces.append(text[end:])
|
|
88
|
+
return ''.join(pieces)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def redact_record(value):
|
|
92
|
+
"""Filter strings at every level of JSON diagnostics, including nested encoded strings."""
|
|
93
|
+
if isinstance(value, str):
|
|
94
|
+
return redact_secrets(value)
|
|
95
|
+
if isinstance(value, dict):
|
|
96
|
+
return {redact_secrets(k) if isinstance(k, str) else k: redact_record(v) for k, v in value.items()}
|
|
97
|
+
if isinstance(value, (list, tuple)):
|
|
98
|
+
return [redact_record(v) for v in value]
|
|
99
|
+
return value
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def redact_stream(source, destination):
|
|
103
|
+
"""Filter JSONL and plain diagnostics, including a bearer split over two plain lines."""
|
|
104
|
+
bearer = False
|
|
105
|
+
for line in source:
|
|
106
|
+
try:
|
|
107
|
+
value = json.loads(line)
|
|
108
|
+
except ValueError:
|
|
109
|
+
if bearer and line.strip():
|
|
110
|
+
line = re.sub(r"^\s*\S+", "<hidden>", line, count=1)
|
|
111
|
+
bearer = False
|
|
112
|
+
bearer = bearer or bool(re.search(r"\bBearer\s*$", line, re.IGNORECASE))
|
|
113
|
+
destination.write(redact_secrets(line))
|
|
114
|
+
else:
|
|
115
|
+
bearer = False
|
|
116
|
+
destination.write(json.dumps(redact_record(value)) + "\n")
|