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
@@ -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")