Bello 0.6.0__py3-none-win_amd64.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.
- bello-0.6.0.data/purelib/supervisor/__init__.py +23 -0
- bello-0.6.0.data/purelib/supervisor/_build.py +9 -0
- bello-0.6.0.data/purelib/supervisor/adversary_agent.py +388 -0
- bello-0.6.0.data/purelib/supervisor/approval_triage.py +510 -0
- bello-0.6.0.data/purelib/supervisor/approvals.py +561 -0
- bello-0.6.0.data/purelib/supervisor/appserver.py +1299 -0
- bello-0.6.0.data/purelib/supervisor/coder.py +409 -0
- bello-0.6.0.data/purelib/supervisor/config_editor.py +2537 -0
- bello-0.6.0.data/purelib/supervisor/controller.py +11084 -0
- bello-0.6.0.data/purelib/supervisor/doctor.py +396 -0
- bello-0.6.0.data/purelib/supervisor/executables.py +186 -0
- bello-0.6.0.data/purelib/supervisor/filesystem_safety.py +283 -0
- bello-0.6.0.data/purelib/supervisor/health.py +196 -0
- bello-0.6.0.data/purelib/supervisor/immutable_reads.py +166 -0
- bello-0.6.0.data/purelib/supervisor/main.py +836 -0
- bello-0.6.0.data/purelib/supervisor/markdown_fences.py +33 -0
- bello-0.6.0.data/purelib/supervisor/pi_worker/LICENSE-Pi +21 -0
- bello-0.6.0.data/purelib/supervisor/pi_worker/NOTICE.md +11 -0
- bello-0.6.0.data/purelib/supervisor/pi_worker/auth.mjs +5 -0
- bello-0.6.0.data/purelib/supervisor/pi_worker/package-lock.json +3422 -0
- bello-0.6.0.data/purelib/supervisor/pi_worker/package.json +26 -0
- bello-0.6.0.data/purelib/supervisor/pi_worker/src/auth-cli.mjs +180 -0
- bello-0.6.0.data/purelib/supervisor/pi_worker/src/pi-sdk.mjs +252 -0
- bello-0.6.0.data/purelib/supervisor/pi_worker/src/protocol.mjs +171 -0
- bello-0.6.0.data/purelib/supervisor/pi_worker/src/runtime.mjs +1076 -0
- bello-0.6.0.data/purelib/supervisor/pi_worker/worker.mjs +39 -0
- bello-0.6.0.data/purelib/supervisor/policy.py +2036 -0
- bello-0.6.0.data/purelib/supervisor/project_config.py +752 -0
- bello-0.6.0.data/purelib/supervisor/prompts/__init__.py +23 -0
- bello-0.6.0.data/purelib/supervisor/prompts/prompts.toml +443 -0
- bello-0.6.0.data/purelib/supervisor/prompts/supervisor.py +277 -0
- bello-0.6.0.data/purelib/supervisor/review_limits.py +60 -0
- bello-0.6.0.data/purelib/supervisor/runtime/__init__.py +1 -0
- bello-0.6.0.data/purelib/supervisor/runtime/bin/THIRD_PARTY_NOTICES.txt +149 -0
- bello-0.6.0.data/purelib/supervisor/runtime/bin/bello-windows-sandbox.exe +0 -0
- bello-0.6.0.data/purelib/supervisor/runtime/claude.py +1384 -0
- bello-0.6.0.data/purelib/supervisor/runtime/cleanup.py +37 -0
- bello-0.6.0.data/purelib/supervisor/runtime/client.py +737 -0
- bello-0.6.0.data/purelib/supervisor/runtime/codex.py +752 -0
- bello-0.6.0.data/purelib/supervisor/runtime/codex_distiller.py +328 -0
- bello-0.6.0.data/purelib/supervisor/runtime/codex_permissions.py +89 -0
- bello-0.6.0.data/purelib/supervisor/runtime/codex_toolchains.py +201 -0
- bello-0.6.0.data/purelib/supervisor/runtime/command_sessions.py +755 -0
- bello-0.6.0.data/purelib/supervisor/runtime/distiller.py +180 -0
- bello-0.6.0.data/purelib/supervisor/runtime/distiller_bundle.py +123 -0
- bello-0.6.0.data/purelib/supervisor/runtime/distiller_download.py +74 -0
- bello-0.6.0.data/purelib/supervisor/runtime/distiller_policy.py +275 -0
- bello-0.6.0.data/purelib/supervisor/runtime/distiller_worker.py +294 -0
- bello-0.6.0.data/purelib/supervisor/runtime/file_worker.py +148 -0
- bello-0.6.0.data/purelib/supervisor/runtime/install.py +163 -0
- bello-0.6.0.data/purelib/supervisor/runtime/journal.py +93 -0
- bello-0.6.0.data/purelib/supervisor/runtime/models.py +60 -0
- bello-0.6.0.data/purelib/supervisor/runtime/native_codex_install.py +454 -0
- bello-0.6.0.data/purelib/supervisor/runtime/output_budget.py +239 -0
- bello-0.6.0.data/purelib/supervisor/runtime/sandbox.py +1397 -0
- bello-0.6.0.data/purelib/supervisor/runtime/tools.json +258 -0
- bello-0.6.0.data/purelib/supervisor/runtime/tools.py +395 -0
- bello-0.6.0.data/purelib/supervisor/runtime/transport.py +210 -0
- bello-0.6.0.data/purelib/supervisor/runtime/windows_sandbox.py +712 -0
- bello-0.6.0.data/purelib/supervisor/runtime_errors.py +184 -0
- bello-0.6.0.data/purelib/supervisor/schemas/__init__.py +101 -0
- bello-0.6.0.data/purelib/supervisor/schemas/models.py +1061 -0
- bello-0.6.0.data/purelib/supervisor/state.py +491 -0
- bello-0.6.0.data/purelib/supervisor/supervisor_agent.py +1678 -0
- bello-0.6.0.data/purelib/supervisor/task_select.py +184 -0
- bello-0.6.0.data/purelib/supervisor/tui.py +93 -0
- bello-0.6.0.data/purelib/supervisor/update_check.py +369 -0
- bello-0.6.0.data/purelib/supervisor/workspace_clean.py +121 -0
- bello-0.6.0.data/purelib/supervisor/workspace_snapshot.py +3663 -0
- bello-0.6.0.dist-info/METADATA +344 -0
- bello-0.6.0.dist-info/RECORD +75 -0
- bello-0.6.0.dist-info/WHEEL +5 -0
- bello-0.6.0.dist-info/entry_points.txt +2 -0
- bello-0.6.0.dist-info/licenses/LICENSE +22 -0
- bello-0.6.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Supervisor Agent MVP package."""
|
|
2
|
+
|
|
3
|
+
__all__ = ["__version__"]
|
|
4
|
+
|
|
5
|
+
import importlib.metadata as _metadata
|
|
6
|
+
import tomllib as _tomllib
|
|
7
|
+
from pathlib import Path as _Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _read_version() -> str:
|
|
11
|
+
try:
|
|
12
|
+
return _metadata.version("bello")
|
|
13
|
+
except _metadata.PackageNotFoundError:
|
|
14
|
+
pyproject = _Path(__file__).resolve().parents[1] / "pyproject.toml"
|
|
15
|
+
try:
|
|
16
|
+
data = _tomllib.loads(pyproject.read_text(encoding="utf-8"))
|
|
17
|
+
except OSError:
|
|
18
|
+
return "0+unknown"
|
|
19
|
+
version = data.get("project", {}).get("version")
|
|
20
|
+
return version if isinstance(version, str) else "0+unknown"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
__version__ = _read_version()
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Build-time Bello source metadata fallback.
|
|
2
|
+
|
|
3
|
+
Packagers may replace these constants during release builds. Runtime update
|
|
4
|
+
checks prefer PEP 610 direct_url.json metadata when it is available.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
SOURCE_URL: str | None = None
|
|
8
|
+
REQUESTED_REVISION: str | None = None
|
|
9
|
+
COMMIT_SHA: str | None = None
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Awaitable, Callable
|
|
7
|
+
|
|
8
|
+
from supervisor.appserver import (
|
|
9
|
+
APP_SERVER_CLEANUP_RPC_TIMEOUT_SECONDS,
|
|
10
|
+
APP_SERVER_CONTROL_RPC_TIMEOUT_SECONDS,
|
|
11
|
+
AppServerClient,
|
|
12
|
+
AppServerError,
|
|
13
|
+
AppServerTimeoutError,
|
|
14
|
+
last_agent_message_text,
|
|
15
|
+
text_input,
|
|
16
|
+
)
|
|
17
|
+
from supervisor.coder import (
|
|
18
|
+
DEFAULT_INTELLIGENCE,
|
|
19
|
+
apply_intelligence,
|
|
20
|
+
apply_multi_agent_thread_start_params,
|
|
21
|
+
)
|
|
22
|
+
from supervisor.project_config import MultiAgentConfig
|
|
23
|
+
from supervisor.prompts import build_adversary_prompt
|
|
24
|
+
from supervisor.schemas import SupervisorWakePacket
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
DEFAULT_ADVERSARY_TIMEOUT_SECONDS = 4000.0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AdversaryAgentError(RuntimeError):
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AdversaryAttemptError(AdversaryAgentError):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class AdversaryRunResult:
|
|
40
|
+
report_text: str
|
|
41
|
+
thread_id: str
|
|
42
|
+
turn_id: str
|
|
43
|
+
candidate_finding: bool
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _denied_probes_retry_note(denied: list[str]) -> str:
|
|
47
|
+
lines = "\n".join(f"- {item}" for item in denied)
|
|
48
|
+
return (
|
|
49
|
+
"Retry note: your previous attempt ended without producing a report after command approval denials. "
|
|
50
|
+
"The following requests were denied:\n"
|
|
51
|
+
f"{lines}\n"
|
|
52
|
+
"Do not request these again and do not work around the denials; treat those areas as unreachable, "
|
|
53
|
+
"record them under not_reached, and complete the report using probes that run inside the snapshot sandbox."
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class AdversaryAgent:
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
client: AppServerClient,
|
|
61
|
+
project_root: Path,
|
|
62
|
+
*,
|
|
63
|
+
model: str | None = None,
|
|
64
|
+
intelligence: str | None = DEFAULT_INTELLIGENCE,
|
|
65
|
+
timeout_seconds: float = DEFAULT_ADVERSARY_TIMEOUT_SECONDS,
|
|
66
|
+
on_thread_start: Callable[[str], None] | None = None,
|
|
67
|
+
on_thread_done: Callable[[str], None] | None = None,
|
|
68
|
+
denied_probes: Callable[[], list[str]] | None = None,
|
|
69
|
+
multi_agent: MultiAgentConfig | None = None,
|
|
70
|
+
before_thread_cleanup: Callable[[str, Path], Awaitable[None]] | None = None,
|
|
71
|
+
):
|
|
72
|
+
self.client = client
|
|
73
|
+
self.project_root = project_root.resolve()
|
|
74
|
+
self.model = model
|
|
75
|
+
self.intelligence = intelligence
|
|
76
|
+
self.timeout_seconds = timeout_seconds
|
|
77
|
+
self.on_thread_start = on_thread_start
|
|
78
|
+
self.on_thread_done = on_thread_done
|
|
79
|
+
self.denied_probes = denied_probes
|
|
80
|
+
self.multi_agent = multi_agent or MultiAgentConfig()
|
|
81
|
+
self.before_thread_cleanup = before_thread_cleanup
|
|
82
|
+
|
|
83
|
+
async def run(
|
|
84
|
+
self,
|
|
85
|
+
packet: SupervisorWakePacket,
|
|
86
|
+
*,
|
|
87
|
+
previous_adversary_report: dict[str, Any] | None = None,
|
|
88
|
+
) -> AdversaryRunResult:
|
|
89
|
+
prompt = build_adversary_prompt(
|
|
90
|
+
packet,
|
|
91
|
+
previous_adversary_report=previous_adversary_report,
|
|
92
|
+
)
|
|
93
|
+
last_error: str | None = None
|
|
94
|
+
for attempt in range(2):
|
|
95
|
+
attempt_prompt = prompt
|
|
96
|
+
if attempt:
|
|
97
|
+
# An approval denial can abort the whole turn before the agent gets a
|
|
98
|
+
# chance to record the area as not_reached, so a blind retry would just
|
|
99
|
+
# replay the same request and die the same way. Tell the fresh thread
|
|
100
|
+
# what was already refused so it routes around it and still reports.
|
|
101
|
+
denied = list(self.denied_probes()) if self.denied_probes else []
|
|
102
|
+
if denied:
|
|
103
|
+
attempt_prompt = prompt + "\n\n" + _denied_probes_retry_note(denied)
|
|
104
|
+
try:
|
|
105
|
+
return await self._run_once(attempt_prompt)
|
|
106
|
+
except AdversaryAttemptError as exc:
|
|
107
|
+
last_error = str(exc)
|
|
108
|
+
if attempt == 1:
|
|
109
|
+
raise
|
|
110
|
+
raise AdversaryAgentError(last_error or "adversary run failed")
|
|
111
|
+
|
|
112
|
+
async def _run_once(self, prompt: str) -> AdversaryRunResult:
|
|
113
|
+
thread_id: str | None = None
|
|
114
|
+
turn_id: str | None = None
|
|
115
|
+
try:
|
|
116
|
+
thread_response = await self._await_rpc(
|
|
117
|
+
"adversary thread/start response",
|
|
118
|
+
self.client.thread_start(self._thread_params(), timeout=APP_SERVER_CONTROL_RPC_TIMEOUT_SECONDS),
|
|
119
|
+
timeout=APP_SERVER_CONTROL_RPC_TIMEOUT_SECONDS,
|
|
120
|
+
)
|
|
121
|
+
thread = thread_response.get("thread", {})
|
|
122
|
+
thread_id = thread.get("id") if isinstance(thread, dict) else None
|
|
123
|
+
if not isinstance(thread_id, str):
|
|
124
|
+
raise AdversaryAgentError("adversary thread/start did not return thread id")
|
|
125
|
+
if self.on_thread_start:
|
|
126
|
+
self.on_thread_start(thread_id)
|
|
127
|
+
|
|
128
|
+
turn_response = await self._await_rpc(
|
|
129
|
+
"adversary turn/start response",
|
|
130
|
+
self.client.turn_start(
|
|
131
|
+
self._turn_params(thread_id, prompt),
|
|
132
|
+
timeout=self.timeout_seconds,
|
|
133
|
+
),
|
|
134
|
+
timeout=self.timeout_seconds,
|
|
135
|
+
)
|
|
136
|
+
turn = turn_response.get("turn", {})
|
|
137
|
+
turn_id_value = turn.get("id") if isinstance(turn, dict) else None
|
|
138
|
+
if not isinstance(turn_id_value, str):
|
|
139
|
+
raise AdversaryAgentError("adversary turn/start did not return turn id")
|
|
140
|
+
turn_id = turn_id_value
|
|
141
|
+
status = turn.get("status")
|
|
142
|
+
if status in {"failed", "interrupted"}:
|
|
143
|
+
raise AdversaryAttemptError(_turn_failure_message(turn, thread_id=thread_id, turn_id=turn_id))
|
|
144
|
+
if status != "completed":
|
|
145
|
+
try:
|
|
146
|
+
completed = await self.client.wait_for_notification(
|
|
147
|
+
lambda message: _is_terminal_turn_notification(
|
|
148
|
+
message,
|
|
149
|
+
thread_id=thread_id,
|
|
150
|
+
turn_id=turn_id,
|
|
151
|
+
),
|
|
152
|
+
timeout=self.timeout_seconds,
|
|
153
|
+
)
|
|
154
|
+
except (asyncio.TimeoutError, AppServerError) as exc:
|
|
155
|
+
if isinstance(exc, (asyncio.TimeoutError, AppServerTimeoutError)):
|
|
156
|
+
detail = f"timed out after {self.timeout_seconds:g}s"
|
|
157
|
+
else:
|
|
158
|
+
detail = f"failed with {exc.__class__.__name__}: {exc}"
|
|
159
|
+
raise AdversaryAttemptError(
|
|
160
|
+
f"adversary turn completion wait {detail} "
|
|
161
|
+
f"thread_id={thread_id} turn_id={turn_id}"
|
|
162
|
+
) from exc
|
|
163
|
+
if completed.method == "error":
|
|
164
|
+
raise AdversaryAttemptError(
|
|
165
|
+
_turn_error_notification_message(
|
|
166
|
+
completed.params,
|
|
167
|
+
thread_id=thread_id,
|
|
168
|
+
turn_id=turn_id,
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
turn = completed.params.get("turn", {})
|
|
172
|
+
if turn.get("status") != "completed":
|
|
173
|
+
raise AdversaryAttemptError(_turn_failure_message(turn, thread_id=thread_id, turn_id=turn_id))
|
|
174
|
+
|
|
175
|
+
text = last_agent_message_text(turn)
|
|
176
|
+
if text is None:
|
|
177
|
+
turns = await self._await_rpc(
|
|
178
|
+
"adversary thread/turns/list response",
|
|
179
|
+
self.client.thread_turns_list(
|
|
180
|
+
thread_id,
|
|
181
|
+
limit=5,
|
|
182
|
+
items_view="full",
|
|
183
|
+
timeout=APP_SERVER_CONTROL_RPC_TIMEOUT_SECONDS,
|
|
184
|
+
),
|
|
185
|
+
timeout=APP_SERVER_CONTROL_RPC_TIMEOUT_SECONDS,
|
|
186
|
+
)
|
|
187
|
+
text = _agent_message_text_from_turns(turns.get("data", []), turn_id=turn_id)
|
|
188
|
+
if text is None or not text.strip():
|
|
189
|
+
raise AdversaryAttemptError("adversary did not produce an agent message")
|
|
190
|
+
report_text = text.strip()
|
|
191
|
+
return AdversaryRunResult(
|
|
192
|
+
report_text=report_text,
|
|
193
|
+
thread_id=thread_id,
|
|
194
|
+
turn_id=turn_id,
|
|
195
|
+
candidate_finding=_report_has_candidate_finding(report_text),
|
|
196
|
+
)
|
|
197
|
+
except AdversaryAgentError:
|
|
198
|
+
raise
|
|
199
|
+
except Exception as exc:
|
|
200
|
+
raise AdversaryAgentError(f"{exc.__class__.__name__}: {exc}") from exc
|
|
201
|
+
finally:
|
|
202
|
+
if thread_id is not None:
|
|
203
|
+
if self.before_thread_cleanup is not None:
|
|
204
|
+
try:
|
|
205
|
+
await self.before_thread_cleanup(thread_id, self.project_root)
|
|
206
|
+
except Exception:
|
|
207
|
+
pass
|
|
208
|
+
try:
|
|
209
|
+
await self.client.thread_archive(thread_id, timeout=APP_SERVER_CLEANUP_RPC_TIMEOUT_SECONDS)
|
|
210
|
+
except Exception:
|
|
211
|
+
try:
|
|
212
|
+
await self.client.thread_unsubscribe(
|
|
213
|
+
thread_id,
|
|
214
|
+
timeout=APP_SERVER_CLEANUP_RPC_TIMEOUT_SECONDS,
|
|
215
|
+
)
|
|
216
|
+
except Exception:
|
|
217
|
+
pass
|
|
218
|
+
if self.on_thread_done:
|
|
219
|
+
self.on_thread_done(thread_id)
|
|
220
|
+
|
|
221
|
+
def _thread_params(self) -> dict[str, Any]:
|
|
222
|
+
params: dict[str, Any] = {
|
|
223
|
+
"cwd": str(self.project_root),
|
|
224
|
+
"runtimeWorkspaceRoots": [str(self.project_root)],
|
|
225
|
+
"approvalPolicy": "on-request",
|
|
226
|
+
"approvalsReviewer": "user",
|
|
227
|
+
"sandbox": "workspace-write",
|
|
228
|
+
"ephemeral": False,
|
|
229
|
+
"experimentalRawEvents": False,
|
|
230
|
+
"persistExtendedHistory": False,
|
|
231
|
+
"config": {},
|
|
232
|
+
}
|
|
233
|
+
apply_multi_agent_thread_start_params(
|
|
234
|
+
params,
|
|
235
|
+
self.multi_agent,
|
|
236
|
+
role="adversary",
|
|
237
|
+
)
|
|
238
|
+
if self.model:
|
|
239
|
+
params["model"] = self.model
|
|
240
|
+
return apply_intelligence(params, self.intelligence)
|
|
241
|
+
|
|
242
|
+
def _turn_params(self, thread_id: str, prompt: str) -> dict[str, Any]:
|
|
243
|
+
params: dict[str, Any] = {
|
|
244
|
+
"threadId": thread_id,
|
|
245
|
+
"input": [text_input(prompt)],
|
|
246
|
+
"cwd": str(self.project_root),
|
|
247
|
+
"runtimeWorkspaceRoots": [str(self.project_root)],
|
|
248
|
+
"approvalPolicy": "on-request",
|
|
249
|
+
"approvalsReviewer": "user",
|
|
250
|
+
"sandboxPolicy": {
|
|
251
|
+
"type": "workspaceWrite",
|
|
252
|
+
"writableRoots": [str(self.project_root)],
|
|
253
|
+
"networkAccess": False,
|
|
254
|
+
},
|
|
255
|
+
}
|
|
256
|
+
if self.model:
|
|
257
|
+
params["model"] = self.model
|
|
258
|
+
return apply_intelligence(params, self.intelligence)
|
|
259
|
+
|
|
260
|
+
async def _await_rpc(self, stage: str, awaitable: Any, *, timeout: float) -> Any:
|
|
261
|
+
try:
|
|
262
|
+
return await asyncio.wait_for(awaitable, timeout=timeout)
|
|
263
|
+
except asyncio.TimeoutError as exc:
|
|
264
|
+
raise AdversaryAgentError(f"{stage} timed out after {timeout:g}s") from exc
|
|
265
|
+
except AdversaryAgentError:
|
|
266
|
+
raise
|
|
267
|
+
except Exception as exc:
|
|
268
|
+
raise AdversaryAgentError(f"{stage} failed with {exc.__class__.__name__}: {exc}") from exc
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _agent_message_text_from_turns(data: Any, *, turn_id: str | None) -> str | None:
|
|
272
|
+
if not isinstance(data, list):
|
|
273
|
+
return None
|
|
274
|
+
for turn in data:
|
|
275
|
+
if not isinstance(turn, dict):
|
|
276
|
+
continue
|
|
277
|
+
if turn_id is not None and turn.get("id") != turn_id:
|
|
278
|
+
continue
|
|
279
|
+
text = last_agent_message_text(turn)
|
|
280
|
+
if text:
|
|
281
|
+
return text
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _is_terminal_turn_notification(message: Any, *, thread_id: str, turn_id: str) -> bool:
|
|
286
|
+
params = message.params
|
|
287
|
+
if params.get("threadId") != thread_id:
|
|
288
|
+
return False
|
|
289
|
+
if message.method == "turn/completed":
|
|
290
|
+
turn = params.get("turn")
|
|
291
|
+
return isinstance(turn, dict) and turn.get("id") == turn_id
|
|
292
|
+
if message.method != "error" or params.get("turnId") != turn_id:
|
|
293
|
+
return False
|
|
294
|
+
# Codex can emit a transient error while it is already retrying the same turn.
|
|
295
|
+
# Let that built-in retry finish; only a terminal error should consume one of
|
|
296
|
+
# Bello's two fresh-thread adversary attempts.
|
|
297
|
+
return params.get("willRetry") is not True
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _turn_error_notification_message(params: dict[str, Any], *, thread_id: str, turn_id: str) -> str:
|
|
301
|
+
error = params.get("error")
|
|
302
|
+
details: list[str] = []
|
|
303
|
+
if isinstance(error, dict):
|
|
304
|
+
message = error.get("message")
|
|
305
|
+
if isinstance(message, str) and message.strip():
|
|
306
|
+
details.append(message.strip())
|
|
307
|
+
codex_error_info = error.get("codexErrorInfo")
|
|
308
|
+
if codex_error_info is not None:
|
|
309
|
+
details.append(f"codexErrorInfo={codex_error_info!r}")
|
|
310
|
+
additional = error.get("additionalDetails")
|
|
311
|
+
if isinstance(additional, str) and additional.strip():
|
|
312
|
+
details.append(additional.strip())
|
|
313
|
+
elif isinstance(error, str) and error.strip():
|
|
314
|
+
details.append(error.strip())
|
|
315
|
+
message = params.get("message")
|
|
316
|
+
if not details and isinstance(message, str) and message.strip():
|
|
317
|
+
details.append(message.strip())
|
|
318
|
+
detail_text = f": {'; '.join(details)}" if details else ""
|
|
319
|
+
return (
|
|
320
|
+
"adversary turn received terminal app-server error "
|
|
321
|
+
f"thread_id={thread_id} turn_id={turn_id}{detail_text}"
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _turn_failure_message(turn: Any, *, thread_id: str, turn_id: str) -> str:
|
|
326
|
+
status = turn.get("status") if isinstance(turn, dict) else None
|
|
327
|
+
error = turn.get("error") if isinstance(turn, dict) else None
|
|
328
|
+
details: list[str] = []
|
|
329
|
+
if isinstance(error, dict):
|
|
330
|
+
message = error.get("message")
|
|
331
|
+
if isinstance(message, str) and message.strip():
|
|
332
|
+
details.append(message.strip())
|
|
333
|
+
codex_error_info = error.get("codexErrorInfo")
|
|
334
|
+
if codex_error_info is not None:
|
|
335
|
+
details.append(f"codexErrorInfo={codex_error_info!r}")
|
|
336
|
+
additional = error.get("additionalDetails")
|
|
337
|
+
if isinstance(additional, str) and additional.strip():
|
|
338
|
+
details.append(additional.strip())
|
|
339
|
+
detail_text = f": {'; '.join(details)}" if details else ""
|
|
340
|
+
return (
|
|
341
|
+
f"adversary turn ended with status={status!r} "
|
|
342
|
+
f"thread_id={thread_id} turn_id={turn_id}{detail_text}"
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _report_has_candidate_finding(report_text: str) -> bool:
|
|
347
|
+
"""Extract informational metadata; every nonempty report goes to the report controller."""
|
|
348
|
+
lowered_lines = [line.strip().lower() for line in report_text.splitlines() if line.strip()]
|
|
349
|
+
for index, line in enumerate(lowered_lines):
|
|
350
|
+
if line.startswith("candidate_finding:"):
|
|
351
|
+
value = line.split(":", 1)[1].strip()
|
|
352
|
+
if value in {"false", "no", "none", "0"}:
|
|
353
|
+
return False
|
|
354
|
+
if value in {"true", "yes", "1"}:
|
|
355
|
+
return True
|
|
356
|
+
if line.startswith("findings:"):
|
|
357
|
+
value = line.split(":", 1)[1].strip()
|
|
358
|
+
if value:
|
|
359
|
+
return value not in {"none", "no", "no findings", "nothing", "n/a", "not found"}
|
|
360
|
+
for following in lowered_lines[index + 1 :]:
|
|
361
|
+
if _looks_like_report_section(following):
|
|
362
|
+
return False
|
|
363
|
+
normalized = following.lstrip("-*0123456789. )").strip()
|
|
364
|
+
if not normalized or normalized in {"none", "no", "no findings", "nothing", "n/a", "not found"}:
|
|
365
|
+
continue
|
|
366
|
+
return True
|
|
367
|
+
return False
|
|
368
|
+
return True
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _looks_like_report_section(line: str) -> bool:
|
|
372
|
+
prefixes = (
|
|
373
|
+
"attacked:",
|
|
374
|
+
"previous_findings_checked:",
|
|
375
|
+
"previous findings checked:",
|
|
376
|
+
"findings:",
|
|
377
|
+
"observations:",
|
|
378
|
+
"held:",
|
|
379
|
+
"not_reached:",
|
|
380
|
+
"not reached:",
|
|
381
|
+
"overall:",
|
|
382
|
+
"candidate_finding:",
|
|
383
|
+
"candidate finding:",
|
|
384
|
+
)
|
|
385
|
+
normalized = line.lstrip("-* \t")
|
|
386
|
+
if any(normalized.startswith(prefix) for prefix in prefixes):
|
|
387
|
+
return True
|
|
388
|
+
return False
|