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.
Files changed (75) hide show
  1. bello-0.6.0.data/purelib/supervisor/__init__.py +23 -0
  2. bello-0.6.0.data/purelib/supervisor/_build.py +9 -0
  3. bello-0.6.0.data/purelib/supervisor/adversary_agent.py +388 -0
  4. bello-0.6.0.data/purelib/supervisor/approval_triage.py +510 -0
  5. bello-0.6.0.data/purelib/supervisor/approvals.py +561 -0
  6. bello-0.6.0.data/purelib/supervisor/appserver.py +1299 -0
  7. bello-0.6.0.data/purelib/supervisor/coder.py +409 -0
  8. bello-0.6.0.data/purelib/supervisor/config_editor.py +2537 -0
  9. bello-0.6.0.data/purelib/supervisor/controller.py +11084 -0
  10. bello-0.6.0.data/purelib/supervisor/doctor.py +396 -0
  11. bello-0.6.0.data/purelib/supervisor/executables.py +186 -0
  12. bello-0.6.0.data/purelib/supervisor/filesystem_safety.py +283 -0
  13. bello-0.6.0.data/purelib/supervisor/health.py +196 -0
  14. bello-0.6.0.data/purelib/supervisor/immutable_reads.py +166 -0
  15. bello-0.6.0.data/purelib/supervisor/main.py +836 -0
  16. bello-0.6.0.data/purelib/supervisor/markdown_fences.py +33 -0
  17. bello-0.6.0.data/purelib/supervisor/pi_worker/LICENSE-Pi +21 -0
  18. bello-0.6.0.data/purelib/supervisor/pi_worker/NOTICE.md +11 -0
  19. bello-0.6.0.data/purelib/supervisor/pi_worker/auth.mjs +5 -0
  20. bello-0.6.0.data/purelib/supervisor/pi_worker/package-lock.json +3422 -0
  21. bello-0.6.0.data/purelib/supervisor/pi_worker/package.json +26 -0
  22. bello-0.6.0.data/purelib/supervisor/pi_worker/src/auth-cli.mjs +180 -0
  23. bello-0.6.0.data/purelib/supervisor/pi_worker/src/pi-sdk.mjs +252 -0
  24. bello-0.6.0.data/purelib/supervisor/pi_worker/src/protocol.mjs +171 -0
  25. bello-0.6.0.data/purelib/supervisor/pi_worker/src/runtime.mjs +1076 -0
  26. bello-0.6.0.data/purelib/supervisor/pi_worker/worker.mjs +39 -0
  27. bello-0.6.0.data/purelib/supervisor/policy.py +2036 -0
  28. bello-0.6.0.data/purelib/supervisor/project_config.py +752 -0
  29. bello-0.6.0.data/purelib/supervisor/prompts/__init__.py +23 -0
  30. bello-0.6.0.data/purelib/supervisor/prompts/prompts.toml +443 -0
  31. bello-0.6.0.data/purelib/supervisor/prompts/supervisor.py +277 -0
  32. bello-0.6.0.data/purelib/supervisor/review_limits.py +60 -0
  33. bello-0.6.0.data/purelib/supervisor/runtime/__init__.py +1 -0
  34. bello-0.6.0.data/purelib/supervisor/runtime/bin/THIRD_PARTY_NOTICES.txt +149 -0
  35. bello-0.6.0.data/purelib/supervisor/runtime/bin/bello-windows-sandbox.exe +0 -0
  36. bello-0.6.0.data/purelib/supervisor/runtime/claude.py +1384 -0
  37. bello-0.6.0.data/purelib/supervisor/runtime/cleanup.py +37 -0
  38. bello-0.6.0.data/purelib/supervisor/runtime/client.py +737 -0
  39. bello-0.6.0.data/purelib/supervisor/runtime/codex.py +752 -0
  40. bello-0.6.0.data/purelib/supervisor/runtime/codex_distiller.py +328 -0
  41. bello-0.6.0.data/purelib/supervisor/runtime/codex_permissions.py +89 -0
  42. bello-0.6.0.data/purelib/supervisor/runtime/codex_toolchains.py +201 -0
  43. bello-0.6.0.data/purelib/supervisor/runtime/command_sessions.py +755 -0
  44. bello-0.6.0.data/purelib/supervisor/runtime/distiller.py +180 -0
  45. bello-0.6.0.data/purelib/supervisor/runtime/distiller_bundle.py +123 -0
  46. bello-0.6.0.data/purelib/supervisor/runtime/distiller_download.py +74 -0
  47. bello-0.6.0.data/purelib/supervisor/runtime/distiller_policy.py +275 -0
  48. bello-0.6.0.data/purelib/supervisor/runtime/distiller_worker.py +294 -0
  49. bello-0.6.0.data/purelib/supervisor/runtime/file_worker.py +148 -0
  50. bello-0.6.0.data/purelib/supervisor/runtime/install.py +163 -0
  51. bello-0.6.0.data/purelib/supervisor/runtime/journal.py +93 -0
  52. bello-0.6.0.data/purelib/supervisor/runtime/models.py +60 -0
  53. bello-0.6.0.data/purelib/supervisor/runtime/native_codex_install.py +454 -0
  54. bello-0.6.0.data/purelib/supervisor/runtime/output_budget.py +239 -0
  55. bello-0.6.0.data/purelib/supervisor/runtime/sandbox.py +1397 -0
  56. bello-0.6.0.data/purelib/supervisor/runtime/tools.json +258 -0
  57. bello-0.6.0.data/purelib/supervisor/runtime/tools.py +395 -0
  58. bello-0.6.0.data/purelib/supervisor/runtime/transport.py +210 -0
  59. bello-0.6.0.data/purelib/supervisor/runtime/windows_sandbox.py +712 -0
  60. bello-0.6.0.data/purelib/supervisor/runtime_errors.py +184 -0
  61. bello-0.6.0.data/purelib/supervisor/schemas/__init__.py +101 -0
  62. bello-0.6.0.data/purelib/supervisor/schemas/models.py +1061 -0
  63. bello-0.6.0.data/purelib/supervisor/state.py +491 -0
  64. bello-0.6.0.data/purelib/supervisor/supervisor_agent.py +1678 -0
  65. bello-0.6.0.data/purelib/supervisor/task_select.py +184 -0
  66. bello-0.6.0.data/purelib/supervisor/tui.py +93 -0
  67. bello-0.6.0.data/purelib/supervisor/update_check.py +369 -0
  68. bello-0.6.0.data/purelib/supervisor/workspace_clean.py +121 -0
  69. bello-0.6.0.data/purelib/supervisor/workspace_snapshot.py +3663 -0
  70. bello-0.6.0.dist-info/METADATA +344 -0
  71. bello-0.6.0.dist-info/RECORD +75 -0
  72. bello-0.6.0.dist-info/WHEEL +5 -0
  73. bello-0.6.0.dist-info/entry_points.txt +2 -0
  74. bello-0.6.0.dist-info/licenses/LICENSE +22 -0
  75. 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