alysis-code 0.13.0__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 (458) hide show
  1. alysis_code/__init__.py +3 -0
  2. alysis_code/__main__.py +4 -0
  3. alysis_code/_build_info.py +14 -0
  4. alysis_code/account_login.py +468 -0
  5. alysis_code/agent/README.md +35 -0
  6. alysis_code/agent/__init__.py +11 -0
  7. alysis_code/agent/acceptance_contract.py +2217 -0
  8. alysis_code/agent/blast_radius.py +1403 -0
  9. alysis_code/agent/cache_keepalive.py +227 -0
  10. alysis_code/agent/completion_certificate.py +366 -0
  11. alysis_code/agent/completion_gate.py +306 -0
  12. alysis_code/agent/empty_response_stall.py +403 -0
  13. alysis_code/agent/errors.py +28 -0
  14. alysis_code/agent/llm_calls.py +475 -0
  15. alysis_code/agent/mutation_classification.py +227 -0
  16. alysis_code/agent/prompt_context.py +2508 -0
  17. alysis_code/agent/read_ledger.py +253 -0
  18. alysis_code/agent/regression_baseline.py +642 -0
  19. alysis_code/agent/reproduction_first.py +610 -0
  20. alysis_code/agent/sensitive_output.py +629 -0
  21. alysis_code/agent/session.py +3218 -0
  22. alysis_code/agent/steering.py +191 -0
  23. alysis_code/agent/subagent_execution.py +5177 -0
  24. alysis_code/agent/subagent_workspace.py +666 -0
  25. alysis_code/agent/tools_assembly.py +4728 -0
  26. alysis_code/agent/turn/__init__.py +101 -0
  27. alysis_code/agent/turn/core.py +8483 -0
  28. alysis_code/agent/turn/events.py +113 -0
  29. alysis_code/agent/turn/exploration.py +590 -0
  30. alysis_code/agent/turn/interventions.py +65 -0
  31. alysis_code/agent/turn/read_cache.py +420 -0
  32. alysis_code/agent/turn/snapshot.py +179 -0
  33. alysis_code/agent/turn_contract.py +661 -0
  34. alysis_code/agent/turn_path.py +129 -0
  35. alysis_code/agent/verification.py +2885 -0
  36. alysis_code/agent/verification_commands.py +512 -0
  37. alysis_code/agent/verification_evidence.py +738 -0
  38. alysis_code/agent_loop.py +693 -0
  39. alysis_code/agent_runtimes/__init__.py +51 -0
  40. alysis_code/agent_runtimes/base.py +114 -0
  41. alysis_code/agent_runtimes/builtins.py +129 -0
  42. alysis_code/agent_runtimes/codex_cli.py +664 -0
  43. alysis_code/agent_runtimes/host.py +263 -0
  44. alysis_code/agent_runtimes/registry.py +64 -0
  45. alysis_code/agent_runtimes/service.py +150 -0
  46. alysis_code/agentbox_client.py +416 -0
  47. alysis_code/agentbox_integration.py +310 -0
  48. alysis_code/alysis_cloud.py +152 -0
  49. alysis_code/approval_scope.py +276 -0
  50. alysis_code/assets/README.md +33 -0
  51. alysis_code/assets/__init__.py +126 -0
  52. alysis_code/assets/asset_read_core.py +281 -0
  53. alysis_code/assets/budget_allocator.py +456 -0
  54. alysis_code/assets/comprehender.py +759 -0
  55. alysis_code/assets/index.py +654 -0
  56. alysis_code/assets/ingestion.py +275 -0
  57. alysis_code/assets/legacy_migration.py +413 -0
  58. alysis_code/assets/models.py +263 -0
  59. alysis_code/assets/ocr.py +239 -0
  60. alysis_code/assets/owl/ascii/f-000.txt +13 -0
  61. alysis_code/assets/owl/ascii/f-001.txt +13 -0
  62. alysis_code/assets/owl/ascii/f-002.txt +13 -0
  63. alysis_code/assets/owl/ascii/f-003.txt +13 -0
  64. alysis_code/assets/owl/ascii/f-004.txt +13 -0
  65. alysis_code/assets/owl/ascii/f-005.txt +13 -0
  66. alysis_code/assets/owl/ascii/f-006.txt +13 -0
  67. alysis_code/assets/owl/ascii/f-007.txt +13 -0
  68. alysis_code/assets/owl/ascii/f-008.txt +13 -0
  69. alysis_code/assets/owl/ascii/f-009.txt +13 -0
  70. alysis_code/assets/owl/ascii/f-010.txt +13 -0
  71. alysis_code/assets/owl/ascii/f-011.txt +13 -0
  72. alysis_code/assets/owl/ascii/f-012.txt +13 -0
  73. alysis_code/assets/owl/ascii/f-013.txt +13 -0
  74. alysis_code/assets/owl/ascii/f-014.txt +13 -0
  75. alysis_code/assets/owl/ascii/f-015.txt +13 -0
  76. alysis_code/assets/owl/ascii/f-016.txt +13 -0
  77. alysis_code/assets/owl/ascii/f-017.txt +13 -0
  78. alysis_code/assets/owl/ascii/f-018.txt +13 -0
  79. alysis_code/assets/owl/ascii/f-019.txt +13 -0
  80. alysis_code/assets/owl/ascii/f-020.txt +13 -0
  81. alysis_code/assets/owl/index.html +98 -0
  82. alysis_code/assets/owl/show-owl.sh +761 -0
  83. alysis_code/assets/paths.py +49 -0
  84. alysis_code/assets/plan_binding.py +326 -0
  85. alysis_code/assets/planner_context.py +466 -0
  86. alysis_code/assets/planner_tools.py +184 -0
  87. alysis_code/assets/prompts.py +101 -0
  88. alysis_code/assets/replanner_context.py +239 -0
  89. alysis_code/assets/surface.py +521 -0
  90. alysis_code/assets/untrusted_content.py +48 -0
  91. alysis_code/assets/usage_logger.py +94 -0
  92. alysis_code/assets/worker_mirror.py +428 -0
  93. alysis_code/assets/worker_section.py +303 -0
  94. alysis_code/assets/worker_tools.py +468 -0
  95. alysis_code/atomic_io.py +83 -0
  96. alysis_code/auth_diagnostics.py +272 -0
  97. alysis_code/background_runner.py +366 -0
  98. alysis_code/branding.py +270 -0
  99. alysis_code/budget_policy.py +390 -0
  100. alysis_code/build_identity.py +465 -0
  101. alysis_code/builtin_hooks/__init__.py +7 -0
  102. alysis_code/builtin_hooks/notify_done_windows.py +65 -0
  103. alysis_code/bwrap_etc.py +76 -0
  104. alysis_code/cancellation.py +41 -0
  105. alysis_code/capabilities.py +137 -0
  106. alysis_code/chatgpt_codex_static_provider.py +133 -0
  107. alysis_code/cli.py +51 -0
  108. alysis_code/cli_impl/__init__.py +1 -0
  109. alysis_code/cli_impl/assets_cli.py +537 -0
  110. alysis_code/cli_impl/assets_modal.py +412 -0
  111. alysis_code/cli_impl/chat/__init__.py +156 -0
  112. alysis_code/cli_impl/chat/commands.py +2616 -0
  113. alysis_code/cli_impl/chat/loop.py +4508 -0
  114. alysis_code/cli_impl/chat/mid_turn_policy.py +125 -0
  115. alysis_code/cli_impl/chat/rendering.py +444 -0
  116. alysis_code/cli_impl/chat/state.py +124 -0
  117. alysis_code/cli_impl/chat_resume.py +830 -0
  118. alysis_code/cli_impl/chat_slash_completer.py +258 -0
  119. alysis_code/cli_impl/commands/__init__.py +11 -0
  120. alysis_code/cli_impl/commands/_shared.py +89 -0
  121. alysis_code/cli_impl/commands/auth.py +623 -0
  122. alysis_code/cli_impl/commands/chat_resume_helpers.py +1531 -0
  123. alysis_code/cli_impl/commands/chat_state.py +158 -0
  124. alysis_code/cli_impl/commands/chat_status.py +1248 -0
  125. alysis_code/cli_impl/commands/chat_terminal.py +942 -0
  126. alysis_code/cli_impl/commands/chat_tui_panels.py +1018 -0
  127. alysis_code/cli_impl/commands/cli_common.py +1223 -0
  128. alysis_code/cli_impl/commands/cli_surface.py +77 -0
  129. alysis_code/cli_impl/commands/config.py +131 -0
  130. alysis_code/cli_impl/commands/conventions.py +85 -0
  131. alysis_code/cli_impl/commands/execution_helpers.py +350 -0
  132. alysis_code/cli_impl/commands/extensions.py +401 -0
  133. alysis_code/cli_impl/commands/forge.py +1282 -0
  134. alysis_code/cli_impl/commands/forge_asset_view.py +121 -0
  135. alysis_code/cli_impl/commands/forge_helpers.py +1215 -0
  136. alysis_code/cli_impl/commands/hooks.py +737 -0
  137. alysis_code/cli_impl/commands/ide_bridge.py +31 -0
  138. alysis_code/cli_impl/commands/mcp.py +700 -0
  139. alysis_code/cli_impl/commands/profile.py +453 -0
  140. alysis_code/cli_impl/commands/prompt_helpers.py +307 -0
  141. alysis_code/cli_impl/commands/report.py +88 -0
  142. alysis_code/cli_impl/commands/root.py +1118 -0
  143. alysis_code/cli_impl/commands/sandbox.py +184 -0
  144. alysis_code/cli_impl/commands/server.py +54 -0
  145. alysis_code/cli_impl/commands/sessions.py +252 -0
  146. alysis_code/cli_impl/commands/skills.py +404 -0
  147. alysis_code/cli_impl/commands/startup.py +946 -0
  148. alysis_code/cli_impl/commands/tools.py +335 -0
  149. alysis_code/cli_impl/commands/update.py +364 -0
  150. alysis_code/cli_impl/commands/welcome.py +972 -0
  151. alysis_code/cli_impl/config_menu.py +3882 -0
  152. alysis_code/cli_impl/forge.py +4509 -0
  153. alysis_code/cli_impl/forge_recovery.py +485 -0
  154. alysis_code/cli_impl/setup_wizard.py +2409 -0
  155. alysis_code/cli_impl/tui/__init__.py +58 -0
  156. alysis_code/cli_impl/tui/app.py +4551 -0
  157. alysis_code/cli_impl/tui/config.py +32 -0
  158. alysis_code/cli_impl/tui/config_flow.py +2754 -0
  159. alysis_code/cli_impl/tui/config_overlay.py +566 -0
  160. alysis_code/cli_impl/tui/content.py +78 -0
  161. alysis_code/cli_impl/tui/footer.py +218 -0
  162. alysis_code/cli_impl/tui/forge_status.py +136 -0
  163. alysis_code/cli_impl/tui/markdown.py +244 -0
  164. alysis_code/cli_impl/tui/owl.py +109 -0
  165. alysis_code/cli_impl/tui/plan_meta.py +477 -0
  166. alysis_code/cli_impl/tui/setup_app.py +519 -0
  167. alysis_code/cli_impl/tui/setup_flow.py +1622 -0
  168. alysis_code/cli_impl/tui/state.py +101 -0
  169. alysis_code/cli_impl/tui/subagent_identity.py +66 -0
  170. alysis_code/cli_impl/tui/subagent_panel.py +186 -0
  171. alysis_code/cli_impl/tui/surface.py +796 -0
  172. alysis_code/cli_impl/tui/transcript.py +514 -0
  173. alysis_code/cli_impl/tui/update_prompt.py +79 -0
  174. alysis_code/cli_impl/tui/workspace_guard.py +384 -0
  175. alysis_code/clipboard.py +172 -0
  176. alysis_code/code_review.py +1211 -0
  177. alysis_code/compaction/__init__.py +28 -0
  178. alysis_code/compaction/conversation_compactor.py +2932 -0
  179. alysis_code/compaction/importance.py +177 -0
  180. alysis_code/compaction/settings.py +297 -0
  181. alysis_code/compaction/tool_output_offload.py +447 -0
  182. alysis_code/config.py +3509 -0
  183. alysis_code/conflict_auto_resolver.py +895 -0
  184. alysis_code/context/__init__.py +1 -0
  185. alysis_code/context/tool_schema_budgeter.py +220 -0
  186. alysis_code/crash_diagnostics.py +282 -0
  187. alysis_code/custom_tools/README.md +34 -0
  188. alysis_code/custom_tools/__init__.py +43 -0
  189. alysis_code/custom_tools/discovery.py +903 -0
  190. alysis_code/custom_tools/runtime.py +1516 -0
  191. alysis_code/custom_tools/session.py +227 -0
  192. alysis_code/custom_tools/trust.py +232 -0
  193. alysis_code/diff_paths.py +113 -0
  194. alysis_code/direction_change.py +293 -0
  195. alysis_code/dispatch_timing.py +306 -0
  196. alysis_code/durable_service_manager.py +1236 -0
  197. alysis_code/edit_discipline.py +659 -0
  198. alysis_code/error_text.py +73 -0
  199. alysis_code/execution_budget.py +411 -0
  200. alysis_code/execution_context.py +915 -0
  201. alysis_code/execution_deadline.py +1065 -0
  202. alysis_code/execution_shared.py +1904 -0
  203. alysis_code/extensions/README.md +30 -0
  204. alysis_code/extensions/__init__.py +93 -0
  205. alysis_code/extensions/activation.py +138 -0
  206. alysis_code/extensions/install.py +1436 -0
  207. alysis_code/extensions/manifest.py +487 -0
  208. alysis_code/extensions/models.py +74 -0
  209. alysis_code/extensions/paths.py +56 -0
  210. alysis_code/extensions/registry.json +4 -0
  211. alysis_code/extensions/registry.py +52 -0
  212. alysis_code/extensions/state.py +83 -0
  213. alysis_code/extensions/workspace_trust.py +101 -0
  214. alysis_code/failed_task_evidence.py +369 -0
  215. alysis_code/failure_category.py +315 -0
  216. alysis_code/feedback_report.py +1647 -0
  217. alysis_code/file_classification.py +485 -0
  218. alysis_code/forge.py +2064 -0
  219. alysis_code/forge_completion.py +362 -0
  220. alysis_code/forge_events.py +475 -0
  221. alysis_code/frontmatter_utils.py +95 -0
  222. alysis_code/git_evidence.py +1181 -0
  223. alysis_code/git_ops.py +560 -0
  224. alysis_code/git_safe.py +62 -0
  225. alysis_code/git_worktrees.py +190 -0
  226. alysis_code/hooks/README.md +33 -0
  227. alysis_code/hooks/__init__.py +67 -0
  228. alysis_code/hooks/audit.py +171 -0
  229. alysis_code/hooks/config.py +225 -0
  230. alysis_code/hooks/dispatcher.py +1110 -0
  231. alysis_code/hooks/models.py +447 -0
  232. alysis_code/hooks/trust.py +202 -0
  233. alysis_code/host_actions.py +543 -0
  234. alysis_code/host_browser.py +103 -0
  235. alysis_code/ide/__init__.py +5 -0
  236. alysis_code/ide/activity_events.py +399 -0
  237. alysis_code/ide/approvals.py +337 -0
  238. alysis_code/ide/artifacts.py +153 -0
  239. alysis_code/ide/browser_egress_proxy.py +1076 -0
  240. alysis_code/ide/cdp_websocket_transport.py +1192 -0
  241. alysis_code/ide/change_ledger.py +1721 -0
  242. alysis_code/ide/context_blocks.py +979 -0
  243. alysis_code/ide/event_stream.py +531 -0
  244. alysis_code/ide/forge_protocol.py +3112 -0
  245. alysis_code/ide/forge_request_ledger.py +737 -0
  246. alysis_code/ide/health.py +965 -0
  247. alysis_code/ide/managed_browser.py +2251 -0
  248. alysis_code/ide/management_protocol.py +3414 -0
  249. alysis_code/ide/mcp_oauth_coordinator.py +744 -0
  250. alysis_code/ide/mcp_oauth_lifecycle.py +1504 -0
  251. alysis_code/ide/prompt_queue.py +1070 -0
  252. alysis_code/ide/protocol.py +191 -0
  253. alysis_code/ide/resumable_swarm.py +1543 -0
  254. alysis_code/ide/session_search.py +295 -0
  255. alysis_code/ide/stdio_bridge.py +9935 -0
  256. alysis_code/ide/structured_state.py +1579 -0
  257. alysis_code/ide/swarm_protocol.py +816 -0
  258. alysis_code/integration_gate.py +506 -0
  259. alysis_code/interactive_input_guard.py +39 -0
  260. alysis_code/interactive_plan_mode.py +26 -0
  261. alysis_code/internal_artifacts.py +179 -0
  262. alysis_code/knowledge_base.py +1409 -0
  263. alysis_code/knowledge_capture.py +1190 -0
  264. alysis_code/knowledge_librarian.py +605 -0
  265. alysis_code/language_policy.py +34 -0
  266. alysis_code/litellm_static_provider.py +535 -0
  267. alysis_code/llm/__init__.py +1 -0
  268. alysis_code/llm/anthropic_messages.py +2288 -0
  269. alysis_code/llm/base.py +71 -0
  270. alysis_code/llm/cache_capabilities.py +985 -0
  271. alysis_code/llm/cache_control_blocks.py +244 -0
  272. alysis_code/llm/cache_policy.py +388 -0
  273. alysis_code/llm/factory.py +373 -0
  274. alysis_code/llm/gemini_generate_content.py +2652 -0
  275. alysis_code/llm/gemini_interactions.py +739 -0
  276. alysis_code/llm/metadata.py +450 -0
  277. alysis_code/llm/openai_compat.py +2947 -0
  278. alysis_code/llm/openai_responses.py +2604 -0
  279. alysis_code/llm/protocols.py +609 -0
  280. alysis_code/llm/provider_limits.py +525 -0
  281. alysis_code/llm/request_plan.py +389 -0
  282. alysis_code/llm/request_shape.py +238 -0
  283. alysis_code/llm/streaming.py +108 -0
  284. alysis_code/llm/temperature_compat.py +78 -0
  285. alysis_code/llm/types.py +195 -0
  286. alysis_code/llm/usage_normalization.py +222 -0
  287. alysis_code/llm_error_display.py +315 -0
  288. alysis_code/logging_redaction.py +326 -0
  289. alysis_code/managed_host_deadline.py +196 -0
  290. alysis_code/mcp/README.md +33 -0
  291. alysis_code/mcp/__init__.py +24 -0
  292. alysis_code/mcp/client.py +1137 -0
  293. alysis_code/mcp/config.py +597 -0
  294. alysis_code/mcp/errors.py +113 -0
  295. alysis_code/mcp/forge_scope.py +154 -0
  296. alysis_code/mcp/jsonrpc.py +214 -0
  297. alysis_code/mcp/manager.py +2308 -0
  298. alysis_code/mcp/models.py +666 -0
  299. alysis_code/mcp/oauth.py +972 -0
  300. alysis_code/mcp/oauth_runtime.py +310 -0
  301. alysis_code/mcp/oauth_store.py +276 -0
  302. alysis_code/mcp/prompts.py +329 -0
  303. alysis_code/mcp/resources.py +295 -0
  304. alysis_code/mcp/roots.py +106 -0
  305. alysis_code/mcp/server_requests.py +75 -0
  306. alysis_code/mcp/token_store.py +859 -0
  307. alysis_code/mcp/transport_http.py +1338 -0
  308. alysis_code/mcp/transport_stdio.py +1267 -0
  309. alysis_code/mcp/untrusted_content.py +119 -0
  310. alysis_code/merge_conflict_reviewer.py +729 -0
  311. alysis_code/model_catalog/__init__.py +1 -0
  312. alysis_code/model_catalog/chatgpt_codex_subscription_snapshot.json +186 -0
  313. alysis_code/model_catalog/litellm_model_prices_snapshot.json +44715 -0
  314. alysis_code/model_catalog/litellm_model_prices_snapshot.meta.json +17 -0
  315. alysis_code/model_metadata_policy.py +223 -0
  316. alysis_code/model_metadata_utils.py +103 -0
  317. alysis_code/model_registry.py +1420 -0
  318. alysis_code/model_router.py +147 -0
  319. alysis_code/permission_policy.py +1016 -0
  320. alysis_code/personas.py +451 -0
  321. alysis_code/pipeline_facts.py +233 -0
  322. alysis_code/plan_assistant.py +4763 -0
  323. alysis_code/plan_mode.py +393 -0
  324. alysis_code/plan_reconciliation.py +1228 -0
  325. alysis_code/plan_repair.py +652 -0
  326. alysis_code/plan_validation.py +1099 -0
  327. alysis_code/planning_constraints.py +904 -0
  328. alysis_code/policy.py +95 -0
  329. alysis_code/preview_server.py +457 -0
  330. alysis_code/process_reaping.py +566 -0
  331. alysis_code/profile_presets.py +1834 -0
  332. alysis_code/profiles.py +666 -0
  333. alysis_code/provider_auth/__init__.py +29 -0
  334. alysis_code/provider_auth/base.py +99 -0
  335. alysis_code/provider_auth/openai_codex.py +951 -0
  336. alysis_code/provider_auth/registry.py +76 -0
  337. alysis_code/provider_auth/store.py +125 -0
  338. alysis_code/provider_diagnostics.py +1209 -0
  339. alysis_code/provider_model_catalog.py +685 -0
  340. alysis_code/provider_telemetry.py +1699 -0
  341. alysis_code/provider_url.py +75 -0
  342. alysis_code/reasoning_contracts.py +911 -0
  343. alysis_code/remote_sync.py +350 -0
  344. alysis_code/replanning.py +1195 -0
  345. alysis_code/repo_scan.py +1152 -0
  346. alysis_code/request_estimation.py +296 -0
  347. alysis_code/review_gate.py +617 -0
  348. alysis_code/run_lock.py +1141 -0
  349. alysis_code/run_outcome.py +58 -0
  350. alysis_code/run_provenance.py +774 -0
  351. alysis_code/run_state.py +445 -0
  352. alysis_code/runtime_artifacts.py +116 -0
  353. alysis_code/runtime_context_features.py +78 -0
  354. alysis_code/runtime_kind.py +52 -0
  355. alysis_code/safety/__init__.py +11 -0
  356. alysis_code/safety/mcp_sanitize.py +29 -0
  357. alysis_code/safety/safe_http.py +297 -0
  358. alysis_code/safety/subagent_report.py +184 -0
  359. alysis_code/sandbox_doctor.py +682 -0
  360. alysis_code/sandbox_runner.py +1025 -0
  361. alysis_code/sandbox_settings.py +423 -0
  362. alysis_code/serialized_paths.py +355 -0
  363. alysis_code/server/__init__.py +3 -0
  364. alysis_code/server/app.py +367 -0
  365. alysis_code/server/auth.py +34 -0
  366. alysis_code/server/job_config.py +30 -0
  367. alysis_code/server/settings.py +215 -0
  368. alysis_code/server/store.py +193 -0
  369. alysis_code/server/worker_runner.py +657 -0
  370. alysis_code/service_persistence.py +355 -0
  371. alysis_code/session_artifacts.py +108 -0
  372. alysis_code/session_metrics.py +331 -0
  373. alysis_code/session_store.py +624 -0
  374. alysis_code/skills/README.md +34 -0
  375. alysis_code/skills/__init__.py +104 -0
  376. alysis_code/skills/conventions.py +84 -0
  377. alysis_code/skills/discovery.py +176 -0
  378. alysis_code/skills/eval_models.py +232 -0
  379. alysis_code/skills/eval_runner.py +372 -0
  380. alysis_code/skills/evals.py +1344 -0
  381. alysis_code/skills/install.py +293 -0
  382. alysis_code/skills/loader.py +118 -0
  383. alysis_code/skills/matching.py +103 -0
  384. alysis_code/skills/models.py +71 -0
  385. alysis_code/skills/paths.py +56 -0
  386. alysis_code/skills/prompting.py +500 -0
  387. alysis_code/skills/scaffold.py +142 -0
  388. alysis_code/skills/state.py +441 -0
  389. alysis_code/skills/transactions.py +125 -0
  390. alysis_code/skills/validation.py +304 -0
  391. alysis_code/step_budget.py +238 -0
  392. alysis_code/subagent_labels.py +49 -0
  393. alysis_code/subagents.py +1072 -0
  394. alysis_code/surface/__init__.py +80 -0
  395. alysis_code/surface/base.py +305 -0
  396. alysis_code/surface/console.py +387 -0
  397. alysis_code/surface/events.py +372 -0
  398. alysis_code/surface/hidden_surface.py +529 -0
  399. alysis_code/surface/noop_surface.py +219 -0
  400. alysis_code/surface/rich_surface.py +1555 -0
  401. alysis_code/surface/styles.py +67 -0
  402. alysis_code/surface/theme.py +455 -0
  403. alysis_code/surface/types.py +100 -0
  404. alysis_code/swarm_backend.py +926 -0
  405. alysis_code/swarm_orchestrator.py +4020 -0
  406. alysis_code/swarm_scheduler.py +441 -0
  407. alysis_code/swarm_trace.py +429 -0
  408. alysis_code/swarm_worker.py +2119 -0
  409. alysis_code/swarm_write_guard.py +348 -0
  410. alysis_code/task_dependencies.py +170 -0
  411. alysis_code/task_readiness.py +992 -0
  412. alysis_code/task_scope.py +2148 -0
  413. alysis_code/terminal_manager.py +762 -0
  414. alysis_code/terminal_ownership.py +460 -0
  415. alysis_code/text_normalization.py +30 -0
  416. alysis_code/token_budget.py +97 -0
  417. alysis_code/tools/README.md +34 -0
  418. alysis_code/tools/__init__.py +1 -0
  419. alysis_code/tools/artifacts.py +127 -0
  420. alysis_code/tools/availability.py +188 -0
  421. alysis_code/tools/fs.py +1456 -0
  422. alysis_code/tools/git.py +461 -0
  423. alysis_code/tools/history.py +229 -0
  424. alysis_code/tools/http_timeout.py +78 -0
  425. alysis_code/tools/image_generation.py +552 -0
  426. alysis_code/tools/registry.py +2936 -0
  427. alysis_code/tools/repo_map.py +476 -0
  428. alysis_code/tools/search.py +563 -0
  429. alysis_code/tools/shell.py +135 -0
  430. alysis_code/tools/symbols.py +1350 -0
  431. alysis_code/tools/test_discovery.py +643 -0
  432. alysis_code/tools/web.py +482 -0
  433. alysis_code/tools/web_search.py +2012 -0
  434. alysis_code/tools/web_search_dashscope.py +557 -0
  435. alysis_code/tools/web_search_ddgs.py +221 -0
  436. alysis_code/tools/web_search_provider_adapters.py +1429 -0
  437. alysis_code/tools/web_search_tavily.py +194 -0
  438. alysis_code/updates.py +933 -0
  439. alysis_code/usage_tracker.py +1990 -0
  440. alysis_code/verification_command_analysis.py +1004 -0
  441. alysis_code/verification_contract.py +574 -0
  442. alysis_code/verification_failure_summary.py +273 -0
  443. alysis_code/verification_repair.py +385 -0
  444. alysis_code/verify_gate.py +3129 -0
  445. alysis_code/web_research.py +1872 -0
  446. alysis_code/web_search_adapters.py +66 -0
  447. alysis_code/web_search_policy.py +27 -0
  448. alysis_code/workspace_binding.py +389 -0
  449. alysis_code/workspace_binding_ui.py +408 -0
  450. alysis_code/workspace_context.py +273 -0
  451. alysis_code/workspace_isolation.py +138 -0
  452. alysis_code/workspace_provisioning.py +455 -0
  453. alysis_code-0.13.0.dist-info/METADATA +507 -0
  454. alysis_code-0.13.0.dist-info/RECORD +458 -0
  455. alysis_code-0.13.0.dist-info/WHEEL +4 -0
  456. alysis_code-0.13.0.dist-info/entry_points.txt +3 -0
  457. alysis_code-0.13.0.dist-info/licenses/LICENSE +176 -0
  458. alysis_code-0.13.0.dist-info/licenses/NOTICE +4 -0
@@ -0,0 +1,1192 @@
1
+ """Bounded, concurrent WebSocket transport for Chromium DevTools Protocol.
2
+
3
+ The managed browser launches Chromium's DevTools endpoint on loopback. This
4
+ module is the production transport for that endpoint. It deliberately owns a
5
+ single reader thread because CDP replies and events share one WebSocket and may
6
+ arrive in any order.
7
+
8
+ Navigation installs a persistent Fetch-domain guard at both request and
9
+ response stages. No HTTP(S) request is continued until the caller's URL policy
10
+ accepts its current URL, including requests started after page load by script
11
+ or an input action. Malformed protocol traffic, an unknown response id, queue
12
+ overflow during interception, timeout, and cancellation all fail closed.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import ipaddress
18
+ import json
19
+ import math
20
+ import queue
21
+ import re
22
+ import threading
23
+ import time
24
+ from collections import deque
25
+ from collections.abc import Callable, Mapping, Sequence
26
+ from dataclasses import dataclass
27
+ from typing import Any, Protocol, TypeAlias
28
+ from urllib.parse import urlsplit
29
+
30
+ from .managed_browser import (
31
+ BrowserCancelledError,
32
+ BrowserDependencyError,
33
+ BrowserError,
34
+ BrowserLaunchError,
35
+ BrowserLimitError,
36
+ BrowserSecurityError,
37
+ BrowserTimeoutError,
38
+ BrowserValidationError,
39
+ CancelCheck,
40
+ CdpTransport,
41
+ CdpTransportFactory,
42
+ )
43
+
44
+ __all__ = [
45
+ "CdpCommandError",
46
+ "WebSocketCdpTransport",
47
+ "WebSocketCdpTransportFactory",
48
+ "validate_cdp_websocket_url",
49
+ ]
50
+
51
+ _MAX_MESSAGE_BYTES = 8 * 1024 * 1024
52
+ _MAX_EVENT_BYTES = 16 * 1024 * 1024
53
+ _MAX_EVENT_COUNT = 512
54
+ _MAX_EVENT_DRAIN = 2_000
55
+ _MAX_METHOD_CHARS = 256
56
+ _MAX_SESSION_ID_CHARS = 256
57
+ _MAX_RESPONSE_ID = (1 << 53) - 1
58
+ _MAX_PENDING_CALLS = 256
59
+ _READER_POLL_SECONDS = 0.2
60
+ _WAIT_POLL_SECONDS = 0.05
61
+ _DEVTOOLS_PATH_RE = re.compile(r"^/devtools/browser/[A-Za-z0-9._-]{1,160}$")
62
+ _METHOD_RE = re.compile(r"^[A-Za-z][A-Za-z0-9_.]{0,255}$")
63
+
64
+
65
+ class CdpCommandError(BrowserError):
66
+ """A CDP command returned an error object.
67
+
68
+ Chromium's error text is intentionally not reflected because it may contain
69
+ page-controlled data. ``code`` is retained when it is a bounded integer.
70
+ """
71
+
72
+ def __init__(self, method: str, code: int | None = None) -> None:
73
+ self.method = method
74
+ self.code = code
75
+ suffix = f" (code {code})" if code is not None else ""
76
+ super().__init__(f"Browser CDP command failed: {method}{suffix}.")
77
+
78
+
79
+ class _WebSocketConnection(Protocol):
80
+ def send(self, message: str | bytes) -> None: ...
81
+
82
+ def recv(self, timeout: float | None = None, *, decode: bool | None = None) -> str | bytes: ...
83
+
84
+ def close(self) -> None: ...
85
+
86
+
87
+ _Connector: TypeAlias = Callable[..., _WebSocketConnection]
88
+
89
+
90
+ @dataclass(slots=True)
91
+ class _PendingCall:
92
+ event: threading.Event
93
+ response: Mapping[str, Any] | None = None
94
+ error: BrowserError | None = None
95
+
96
+
97
+ @dataclass(slots=True)
98
+ class _OutboundMessage:
99
+ encoded: str
100
+ sent: threading.Event
101
+ error: BrowserError | None = None
102
+
103
+
104
+ @dataclass(frozen=True, slots=True)
105
+ class _QueuedEvent:
106
+ payload: Mapping[str, Any]
107
+ size_bytes: int
108
+ session_id: str | None
109
+
110
+
111
+ @dataclass(frozen=True, slots=True)
112
+ class _InterceptionPolicy:
113
+ authorize_url: Callable[[str], str]
114
+ timeout: float
115
+
116
+
117
+ def validate_cdp_websocket_url(websocket_url: str) -> str:
118
+ """Accept only an uncredentialed ``ws://`` URL on a literal loopback IP."""
119
+
120
+ if not isinstance(websocket_url, str) or not websocket_url:
121
+ raise BrowserValidationError("Browser DevTools WebSocket URL is invalid.")
122
+ if len(websocket_url) > 512 or any(char.isspace() for char in websocket_url):
123
+ raise BrowserValidationError("Browser DevTools WebSocket URL is invalid.")
124
+ try:
125
+ parsed = urlsplit(websocket_url)
126
+ host = parsed.hostname
127
+ port = parsed.port
128
+ except ValueError as exc:
129
+ raise BrowserValidationError("Browser DevTools WebSocket URL is invalid.") from exc
130
+ if (
131
+ parsed.scheme != "ws"
132
+ or parsed.username is not None
133
+ or parsed.password is not None
134
+ or parsed.query
135
+ or parsed.fragment
136
+ or host is None
137
+ or port is None
138
+ or not 1 <= port <= 65_535
139
+ or not _DEVTOOLS_PATH_RE.fullmatch(parsed.path)
140
+ ):
141
+ raise BrowserSecurityError("Browser DevTools WebSocket endpoint is not trusted.")
142
+ try:
143
+ address = ipaddress.ip_address(host)
144
+ except ValueError as exc:
145
+ raise BrowserSecurityError(
146
+ "Browser DevTools WebSocket endpoint must use a literal loopback address."
147
+ ) from exc
148
+ if not address.is_loopback:
149
+ raise BrowserSecurityError("Browser DevTools WebSocket endpoint must be loopback-only.")
150
+ return websocket_url
151
+
152
+
153
+ class WebSocketCdpTransportFactory(CdpTransportFactory):
154
+ """Create :class:`WebSocketCdpTransport` instances with ``websockets``.
155
+
156
+ ``connector`` is injectable for deterministic tests. The default is the
157
+ maintained synchronous client from the ``websockets`` package. Proxying and
158
+ compression are disabled for the private loopback connection.
159
+ """
160
+
161
+ def __init__(
162
+ self,
163
+ *,
164
+ connector: _Connector | None = None,
165
+ max_event_count: int = _MAX_EVENT_COUNT,
166
+ max_event_bytes: int = _MAX_EVENT_BYTES,
167
+ ) -> None:
168
+ if not 1 <= max_event_count <= _MAX_EVENT_COUNT:
169
+ raise BrowserValidationError("Browser CDP event count limit is invalid.")
170
+ if not _MAX_MESSAGE_BYTES <= max_event_bytes <= _MAX_EVENT_BYTES:
171
+ raise BrowserValidationError("Browser CDP event byte limit is invalid.")
172
+ self._connector = connector
173
+ self._max_event_count = max_event_count
174
+ self._max_event_bytes = max_event_bytes
175
+
176
+ def connect(
177
+ self,
178
+ websocket_url: str,
179
+ *,
180
+ timeout: float,
181
+ cancel: CancelCheck = None,
182
+ ) -> CdpTransport:
183
+ endpoint = validate_cdp_websocket_url(websocket_url)
184
+ call_timeout = _validated_timeout(timeout)
185
+ _check_cancel(cancel)
186
+ connector = self._connector or _default_connector()
187
+ finished = threading.Event()
188
+ abandoned = threading.Event()
189
+ outcome_lock = threading.Lock()
190
+ outcome: dict[str, Any] = {}
191
+
192
+ def open_connection() -> None:
193
+ connection: _WebSocketConnection | None = None
194
+ try:
195
+ connection = connector(
196
+ endpoint,
197
+ open_timeout=call_timeout,
198
+ close_timeout=min(call_timeout, 2.0),
199
+ ping_interval=20.0,
200
+ ping_timeout=min(call_timeout, 10.0),
201
+ max_size=_MAX_MESSAGE_BYTES,
202
+ max_queue=(32, 8),
203
+ compression=None,
204
+ proxy=None,
205
+ )
206
+ with outcome_lock:
207
+ if abandoned.is_set():
208
+ _close_connection_async(connection)
209
+ else:
210
+ outcome["connection"] = connection
211
+ except BaseException as exc: # noqa: BLE001 - normalized below
212
+ with outcome_lock:
213
+ outcome["error"] = exc
214
+ finally:
215
+ finished.set()
216
+
217
+ worker = threading.Thread(
218
+ target=open_connection,
219
+ name="alysis-cdp-connect",
220
+ daemon=True,
221
+ )
222
+ worker.start()
223
+ deadline = time.monotonic() + call_timeout
224
+ while not finished.wait(
225
+ timeout=min(_WAIT_POLL_SECONDS, max(0.0, deadline - time.monotonic()))
226
+ ):
227
+ if cancel is not None and cancel():
228
+ with outcome_lock:
229
+ abandoned.set()
230
+ late_connection = outcome.pop("connection", None)
231
+ if late_connection is not None:
232
+ _close_connection_async(late_connection)
233
+ raise BrowserCancelledError("Browser WebSocket connection was cancelled.")
234
+ if time.monotonic() >= deadline:
235
+ with outcome_lock:
236
+ abandoned.set()
237
+ late_connection = outcome.pop("connection", None)
238
+ if late_connection is not None:
239
+ _close_connection_async(late_connection)
240
+ raise BrowserTimeoutError("Browser WebSocket connection timed out.")
241
+ with outcome_lock:
242
+ connection = outcome.get("connection")
243
+ if cancel is not None and cancel():
244
+ with outcome_lock:
245
+ abandoned.set()
246
+ if connection is not None:
247
+ _close_connection_async(connection)
248
+ raise BrowserCancelledError("Browser WebSocket connection was cancelled.")
249
+ if connection is None:
250
+ raise BrowserLaunchError(
251
+ "Browser DevTools WebSocket connection failed."
252
+ ) from outcome.get("error")
253
+ return WebSocketCdpTransport(
254
+ connection,
255
+ max_event_count=self._max_event_count,
256
+ max_event_bytes=self._max_event_bytes,
257
+ )
258
+
259
+
260
+ class WebSocketCdpTransport(CdpTransport):
261
+ """Thread-safe, bounded CDP request/reply and event multiplexer."""
262
+
263
+ def __init__(
264
+ self,
265
+ connection: _WebSocketConnection,
266
+ *,
267
+ max_event_count: int = _MAX_EVENT_COUNT,
268
+ max_event_bytes: int = _MAX_EVENT_BYTES,
269
+ ) -> None:
270
+ if not 1 <= max_event_count <= _MAX_EVENT_COUNT:
271
+ raise BrowserValidationError("Browser CDP event count limit is invalid.")
272
+ if not _MAX_MESSAGE_BYTES <= max_event_bytes <= _MAX_EVENT_BYTES:
273
+ raise BrowserValidationError("Browser CDP event byte limit is invalid.")
274
+ self._connection = connection
275
+ self._max_event_count = max_event_count
276
+ self._max_event_bytes = max_event_bytes
277
+ self._lock = threading.RLock()
278
+ self._event_changed = threading.Condition(self._lock)
279
+ self._navigation_lock = threading.Lock()
280
+ self._pending: dict[int, _PendingCall] = {}
281
+ self._outbound: queue.Queue[_OutboundMessage] = queue.Queue(maxsize=_MAX_PENDING_CALLS)
282
+ self._interception_events: queue.Queue[_QueuedEvent] = queue.Queue(maxsize=max_event_count)
283
+ self._interception_event_bytes = 0
284
+ self._interception_policies: dict[str, _InterceptionPolicy] = {}
285
+ self._events: deque[_QueuedEvent] = deque()
286
+ self._event_bytes = 0
287
+ self._dropped_events = 0
288
+ self._drop_generation = 0
289
+ self._next_id = 1
290
+ self._terminal_error: BrowserError | None = None
291
+ self._closed = False
292
+ self._reader = threading.Thread(
293
+ target=self._reader_loop,
294
+ name="alysis-cdp-reader",
295
+ daemon=True,
296
+ )
297
+ self._writer = threading.Thread(
298
+ target=self._writer_loop,
299
+ name="alysis-cdp-writer",
300
+ daemon=True,
301
+ )
302
+ self._interceptor = threading.Thread(
303
+ target=self._interceptor_loop,
304
+ name="alysis-cdp-interceptor",
305
+ daemon=True,
306
+ )
307
+ self._reader.start()
308
+ self._writer.start()
309
+ self._interceptor.start()
310
+
311
+ @property
312
+ def dropped_event_count(self) -> int:
313
+ with self._lock:
314
+ return self._dropped_events
315
+
316
+ def call(
317
+ self,
318
+ method: str,
319
+ params: Mapping[str, Any] | None = None,
320
+ *,
321
+ session_id: str | None = None,
322
+ timeout: float,
323
+ cancel: CancelCheck = None,
324
+ ) -> Mapping[str, Any]:
325
+ normalized_method = _validated_method(method)
326
+ normalized_session = _validated_session_id(session_id)
327
+ call_timeout = _validated_timeout(timeout)
328
+ _check_cancel(cancel)
329
+ deadline = time.monotonic() + call_timeout
330
+ with self._lock:
331
+ self._raise_if_unavailable_locked()
332
+ if len(self._pending) >= _MAX_PENDING_CALLS:
333
+ raise BrowserLimitError("Browser CDP pending command capacity was exhausted.")
334
+ request_id = self._next_request_id_locked()
335
+ pending = _PendingCall(event=threading.Event())
336
+ self._pending[request_id] = pending
337
+ message: dict[str, Any] = {
338
+ "id": request_id,
339
+ "method": normalized_method,
340
+ "params": dict(params or {}),
341
+ }
342
+ if normalized_session is not None:
343
+ message["sessionId"] = normalized_session
344
+ try:
345
+ encoded = _encode_message(message)
346
+ except BaseException:
347
+ with self._lock:
348
+ self._pending.pop(request_id, None)
349
+ raise
350
+ outbound = _OutboundMessage(encoded=encoded, sent=threading.Event())
351
+ try:
352
+ self._outbound.put_nowait(outbound)
353
+ except queue.Full as exc:
354
+ with self._lock:
355
+ self._pending.pop(request_id, None)
356
+ raise BrowserLimitError("Browser CDP outbound command capacity was exhausted.") from exc
357
+
358
+ while not outbound.sent.wait(
359
+ timeout=min(_WAIT_POLL_SECONDS, max(0.0, deadline - time.monotonic()))
360
+ ):
361
+ if pending.event.is_set():
362
+ break
363
+ if cancel is not None and cancel():
364
+ error = BrowserCancelledError("Browser CDP command was cancelled.")
365
+ self._fail_transport(error)
366
+ raise error
367
+ if time.monotonic() >= deadline:
368
+ error = BrowserTimeoutError("Browser CDP command timed out.")
369
+ self._fail_transport(error)
370
+ raise error
371
+ if outbound.error is not None:
372
+ raise outbound.error
373
+ while not pending.event.wait(
374
+ timeout=min(_WAIT_POLL_SECONDS, max(0.0, deadline - time.monotonic()))
375
+ ):
376
+ if cancel is not None and cancel():
377
+ error = BrowserCancelledError("Browser CDP command was cancelled.")
378
+ self._fail_transport(error)
379
+ raise error
380
+ if time.monotonic() >= deadline:
381
+ error = BrowserTimeoutError("Browser CDP command timed out.")
382
+ self._fail_transport(error)
383
+ raise error
384
+ if pending.error is not None:
385
+ raise pending.error
386
+ response = pending.response
387
+ if response is None:
388
+ error = BrowserSecurityError("Browser CDP response was missing.")
389
+ self._fail_transport(error)
390
+ raise error
391
+ error_payload = response.get("error")
392
+ if error_payload is not None:
393
+ code: int | None = None
394
+ if isinstance(error_payload, Mapping):
395
+ candidate = error_payload.get("code")
396
+ if (
397
+ isinstance(candidate, int)
398
+ and not isinstance(candidate, bool)
399
+ and abs(candidate) <= 1_000_000
400
+ ):
401
+ code = candidate
402
+ raise CdpCommandError(normalized_method, code)
403
+ result = response.get("result", {})
404
+ if not isinstance(result, Mapping):
405
+ error = BrowserSecurityError("Browser CDP result was invalid.")
406
+ self._fail_transport(error)
407
+ raise error
408
+ return dict(result)
409
+
410
+ def guarded_navigate(
411
+ self,
412
+ url: str,
413
+ *,
414
+ session_id: str,
415
+ authorize_url: Callable[[str], str],
416
+ timeout: float,
417
+ cancel: CancelCheck = None,
418
+ ) -> Mapping[str, Any]:
419
+ normalized_session = _validated_session_id(session_id)
420
+ if normalized_session is None:
421
+ raise BrowserValidationError("Browser CDP session id is required for navigation.")
422
+ if not isinstance(url, str) or not url:
423
+ raise BrowserValidationError("Browser navigation URL is invalid.")
424
+ if not callable(authorize_url):
425
+ raise BrowserValidationError("Browser navigation URL policy is invalid.")
426
+ call_timeout = _validated_timeout(timeout)
427
+ _check_cancel(cancel)
428
+ if not self._navigation_lock.acquire(blocking=False):
429
+ raise BrowserSecurityError("Another guarded browser navigation is already running.")
430
+ deadline = time.monotonic() + call_timeout
431
+ navigation_done = threading.Event()
432
+ navigation_outcome: dict[str, Any] = {}
433
+ navigation_started = False
434
+ try:
435
+ initial_url = _authorize(authorize_url, url)
436
+ policy = _InterceptionPolicy(
437
+ authorize_url=authorize_url,
438
+ timeout=min(call_timeout, 30.0),
439
+ )
440
+ with self._lock:
441
+ self._raise_if_unavailable_locked()
442
+ self._interception_policies[normalized_session] = policy
443
+ self.call(
444
+ "Target.setAutoAttach",
445
+ {
446
+ "autoAttach": True,
447
+ "waitForDebuggerOnStart": True,
448
+ "flatten": True,
449
+ },
450
+ session_id=normalized_session,
451
+ timeout=_remaining(deadline),
452
+ cancel=cancel,
453
+ )
454
+ self.call(
455
+ "Page.setLifecycleEventsEnabled",
456
+ {"enabled": True},
457
+ session_id=normalized_session,
458
+ timeout=_remaining(deadline),
459
+ cancel=cancel,
460
+ )
461
+ self.call(
462
+ "Fetch.enable",
463
+ {
464
+ "patterns": [
465
+ {"urlPattern": "*", "requestStage": "Request"},
466
+ {"urlPattern": "*", "requestStage": "Response"},
467
+ ],
468
+ "handleAuthRequests": False,
469
+ },
470
+ session_id=normalized_session,
471
+ timeout=_remaining(deadline),
472
+ cancel=cancel,
473
+ )
474
+ with self._lock:
475
+ drop_generation = self._drop_generation
476
+
477
+ def navigate() -> None:
478
+ try:
479
+ navigation_outcome["result"] = self.call(
480
+ "Page.navigate",
481
+ {"url": initial_url},
482
+ session_id=normalized_session,
483
+ timeout=_remaining(deadline),
484
+ cancel=cancel,
485
+ )
486
+ except BaseException as exc: # noqa: BLE001 - delivered to caller below
487
+ navigation_outcome["error"] = exc
488
+ finally:
489
+ navigation_done.set()
490
+
491
+ threading.Thread(
492
+ target=navigate,
493
+ name="alysis-cdp-navigate",
494
+ daemon=True,
495
+ ).start()
496
+ navigation_started = True
497
+ loaded_loader_ids: set[str] = set()
498
+ while True:
499
+ _check_cancel_and_deadline(cancel, deadline, self)
500
+ with self._lock:
501
+ if self._drop_generation != drop_generation:
502
+ error = BrowserSecurityError(
503
+ "Browser CDP events overflowed during guarded navigation."
504
+ )
505
+ self._fail_transport(error)
506
+ raise error
507
+ event = self._take_navigation_event(
508
+ normalized_session,
509
+ timeout=min(_WAIT_POLL_SECONDS, _remaining(deadline)),
510
+ )
511
+ if event is not None:
512
+ method = event.get("method")
513
+ if method == "Page.lifecycleEvent":
514
+ params = event.get("params")
515
+ if isinstance(params, Mapping) and params.get("name") == "load":
516
+ loader_id = params.get("loaderId")
517
+ if isinstance(loader_id, str) and loader_id:
518
+ loaded_loader_ids.add(loader_id)
519
+ if navigation_done.is_set():
520
+ navigation_error = navigation_outcome.get("error")
521
+ if navigation_error is not None:
522
+ if isinstance(navigation_error, BaseException):
523
+ raise navigation_error
524
+ raise BrowserLaunchError("Browser navigation failed.")
525
+ result = navigation_outcome.get("result")
526
+ if not isinstance(result, Mapping):
527
+ error = BrowserSecurityError("Browser navigation result was invalid.")
528
+ self._fail_transport(error)
529
+ raise error
530
+ error_text = result.get("errorText")
531
+ if error_text:
532
+ raise BrowserLaunchError(
533
+ "Browser navigation did not complete successfully."
534
+ )
535
+ loader_id = result.get("loaderId")
536
+ if not isinstance(loader_id, str) or not loader_id:
537
+ error = BrowserSecurityError(
538
+ "Browser navigation did not provide a trusted document loader id."
539
+ )
540
+ self._fail_transport(error)
541
+ raise error
542
+ if loader_id in loaded_loader_ids:
543
+ return dict(result)
544
+ except BrowserCancelledError as exc:
545
+ self._fail_transport(exc)
546
+ raise
547
+ except BrowserTimeoutError as exc:
548
+ self._fail_transport(exc)
549
+ raise
550
+ except BrowserError as exc:
551
+ # A setup or navigation failure can leave lifecycle/interception
552
+ # state ambiguous. Never allow the transport to continue without
553
+ # a proven persistent guard.
554
+ self._fail_transport(exc)
555
+ raise
556
+ finally:
557
+ if navigation_started and not navigation_done.is_set():
558
+ # Never leave a Page.navigate command running after its guarded
559
+ # operation returned. Its late reply would otherwise race a
560
+ # future operation and leave interception state ambiguous.
561
+ self._fail_transport(BrowserLaunchError("Browser navigation did not stop cleanly."))
562
+ self._navigation_lock.release()
563
+
564
+ def drain_events(
565
+ self,
566
+ *,
567
+ session_id: str,
568
+ max_events: int,
569
+ timeout: float,
570
+ cancel: CancelCheck = None,
571
+ ) -> Sequence[Mapping[str, Any]]:
572
+ normalized_session = _validated_session_id(session_id)
573
+ if normalized_session is None:
574
+ raise BrowserValidationError("Browser CDP session id is required.")
575
+ if (
576
+ isinstance(max_events, bool)
577
+ or not isinstance(max_events, int)
578
+ or not 1 <= max_events <= _MAX_EVENT_DRAIN
579
+ ):
580
+ raise BrowserValidationError("Browser CDP event limit is invalid.")
581
+ call_timeout = _validated_timeout(timeout)
582
+ deadline = time.monotonic() + call_timeout
583
+ output: list[Mapping[str, Any]] = []
584
+ with self._event_changed:
585
+ while True:
586
+ self._raise_if_unavailable_locked()
587
+ if self._dropped_events and len(output) < max_events:
588
+ output.append(
589
+ {
590
+ "method": "Alysis.eventsDropped",
591
+ "params": {"count": self._dropped_events},
592
+ "sessionId": normalized_session,
593
+ }
594
+ )
595
+ self._dropped_events = 0
596
+ if len(output) < max_events:
597
+ retained: deque[_QueuedEvent] = deque()
598
+ while self._events:
599
+ item = self._events.popleft()
600
+ if item.session_id == normalized_session and len(output) < max_events:
601
+ output.append(item.payload)
602
+ self._event_bytes -= item.size_bytes
603
+ else:
604
+ retained.append(item)
605
+ self._events = retained
606
+ if output or time.monotonic() >= deadline:
607
+ return output
608
+ if cancel is not None and cancel():
609
+ raise BrowserCancelledError("Browser event read was cancelled.")
610
+ self._event_changed.wait(
611
+ timeout=min(_WAIT_POLL_SECONDS, max(0.0, deadline - time.monotonic()))
612
+ )
613
+
614
+ def close(self, *, timeout: float) -> None:
615
+ close_timeout = _validated_timeout(timeout)
616
+ self._fail_transport(BrowserLaunchError("Browser DevTools WebSocket was closed."))
617
+ _close_connection_bounded(self._connection, close_timeout)
618
+ if threading.current_thread() is not self._reader:
619
+ self._reader.join(timeout=close_timeout)
620
+ if threading.current_thread() is not self._writer:
621
+ self._writer.join(timeout=close_timeout)
622
+ if threading.current_thread() is not self._interceptor:
623
+ self._interceptor.join(timeout=close_timeout)
624
+
625
+ def _writer_loop(self) -> None:
626
+ while True:
627
+ with self._lock:
628
+ if self._closed:
629
+ return
630
+ try:
631
+ outbound = self._outbound.get(timeout=_READER_POLL_SECONDS)
632
+ except queue.Empty:
633
+ continue
634
+ with self._lock:
635
+ if self._closed:
636
+ outbound.error = self._terminal_error or BrowserLaunchError(
637
+ "Browser DevTools WebSocket is closed."
638
+ )
639
+ outbound.sent.set()
640
+ continue
641
+ try:
642
+ self._connection.send(outbound.encoded)
643
+ except BaseException: # noqa: BLE001 - normalized below
644
+ error = BrowserLaunchError("Browser DevTools WebSocket send failed.")
645
+ outbound.error = error
646
+ outbound.sent.set()
647
+ self._fail_transport(error)
648
+ return
649
+ outbound.sent.set()
650
+
651
+ def _reader_loop(self) -> None:
652
+ while True:
653
+ with self._lock:
654
+ if self._closed:
655
+ return
656
+ try:
657
+ raw = self._connection.recv(timeout=_READER_POLL_SECONDS, decode=False)
658
+ except TimeoutError:
659
+ continue
660
+ except BaseException: # noqa: BLE001 - normalized below
661
+ with self._lock:
662
+ if self._closed:
663
+ return
664
+ error = BrowserLaunchError("Browser DevTools WebSocket receive failed.")
665
+ self._fail_transport(error)
666
+ _close_connection_async(self._connection)
667
+ return
668
+ try:
669
+ payload, size_bytes = _decode_message(raw)
670
+ self._route_message(payload, size_bytes=size_bytes)
671
+ except BrowserError as exc:
672
+ self._fail_transport(exc)
673
+ _close_connection_async(self._connection)
674
+ return
675
+ except BaseException: # noqa: BLE001 - decoder bugs fail closed
676
+ error = BrowserSecurityError("Browser CDP message was invalid.")
677
+ self._fail_transport(error)
678
+ _close_connection_async(self._connection)
679
+ return
680
+
681
+ def _interceptor_loop(self) -> None:
682
+ """Continue every policy-approved request for the transport lifetime."""
683
+
684
+ while True:
685
+ with self._lock:
686
+ if self._closed:
687
+ return
688
+ try:
689
+ item = self._interception_events.get(timeout=_READER_POLL_SECONDS)
690
+ except queue.Empty:
691
+ continue
692
+ with self._lock:
693
+ self._interception_event_bytes -= item.size_bytes
694
+ if self._interception_event_bytes < 0:
695
+ error = BrowserSecurityError(
696
+ "Browser request interception accounting failed closed."
697
+ )
698
+ self._interception_event_bytes = 0
699
+ self._fail_transport(error)
700
+ return
701
+ session_id = item.session_id
702
+ if session_id is None:
703
+ error = BrowserSecurityError("Browser intercepted request had no session id.")
704
+ self._fail_transport(error)
705
+ return
706
+ with self._lock:
707
+ policy = self._interception_policies.get(session_id)
708
+ if policy is None:
709
+ error = BrowserSecurityError("Browser request interception policy was unavailable.")
710
+ self._fail_transport(error)
711
+ return
712
+ try:
713
+ method = item.payload.get("method")
714
+ if method == "Target.attachedToTarget":
715
+ self._guard_attached_target(
716
+ item.payload,
717
+ parent_session_id=session_id,
718
+ policy=policy,
719
+ )
720
+ continue
721
+ if method == "Target.detachedFromTarget":
722
+ self._forget_detached_target(item.payload)
723
+ continue
724
+ if method == "Fetch.authRequired":
725
+ raise BrowserSecurityError(
726
+ "Browser authentication challenge was blocked during navigation."
727
+ )
728
+ if method != "Fetch.requestPaused":
729
+ raise BrowserSecurityError("Browser interception event was invalid.")
730
+ _continue_paused_request(
731
+ self,
732
+ item.payload,
733
+ session_id=session_id,
734
+ authorize_url=policy.authorize_url,
735
+ deadline=time.monotonic() + policy.timeout,
736
+ cancel=None,
737
+ )
738
+ except BrowserError as exc:
739
+ self._fail_transport(exc)
740
+ return
741
+ except BaseException as exc: # noqa: BLE001 - policy broker fails closed
742
+ error = BrowserSecurityError("Browser request interception failed closed.")
743
+ self._fail_transport(error)
744
+ _close_connection_async(self._connection)
745
+ _ = exc
746
+ return
747
+
748
+ def _guard_attached_target(
749
+ self,
750
+ event: Mapping[str, Any],
751
+ *,
752
+ parent_session_id: str,
753
+ policy: _InterceptionPolicy,
754
+ ) -> None:
755
+ """Install the same guard before a popup/worker child is resumed."""
756
+
757
+ params = event.get("params")
758
+ if not isinstance(params, Mapping):
759
+ raise BrowserSecurityError("Browser attached-target event was invalid.")
760
+ child_session_id = params.get("sessionId")
761
+ waiting = params.get("waitingForDebugger")
762
+ if (
763
+ not isinstance(child_session_id, str)
764
+ or not child_session_id
765
+ or len(child_session_id) > _MAX_SESSION_ID_CHARS
766
+ or waiting is not True
767
+ or child_session_id == parent_session_id
768
+ ):
769
+ raise BrowserSecurityError("Browser attached target was not safely paused.")
770
+ with self._lock:
771
+ self._raise_if_unavailable_locked()
772
+ self._interception_policies[child_session_id] = policy
773
+ deadline = time.monotonic() + policy.timeout
774
+ self.call(
775
+ "Target.setAutoAttach",
776
+ {
777
+ "autoAttach": True,
778
+ "waitForDebuggerOnStart": True,
779
+ "flatten": True,
780
+ },
781
+ session_id=child_session_id,
782
+ timeout=_remaining(deadline),
783
+ )
784
+ self.call(
785
+ "Fetch.enable",
786
+ {
787
+ "patterns": [
788
+ {"urlPattern": "*", "requestStage": "Request"},
789
+ {"urlPattern": "*", "requestStage": "Response"},
790
+ ],
791
+ "handleAuthRequests": False,
792
+ },
793
+ session_id=child_session_id,
794
+ timeout=_remaining(deadline),
795
+ )
796
+ self.call(
797
+ "Runtime.runIfWaitingForDebugger",
798
+ {},
799
+ session_id=child_session_id,
800
+ timeout=_remaining(deadline),
801
+ )
802
+
803
+ def _forget_detached_target(self, event: Mapping[str, Any]) -> None:
804
+ params = event.get("params")
805
+ if not isinstance(params, Mapping):
806
+ raise BrowserSecurityError("Browser detached-target event was invalid.")
807
+ child_session_id = params.get("sessionId")
808
+ if not isinstance(child_session_id, str) or not child_session_id:
809
+ raise BrowserSecurityError("Browser detached-target event was invalid.")
810
+ with self._lock:
811
+ self._interception_policies.pop(child_session_id, None)
812
+
813
+ def _route_message(self, payload: Mapping[str, Any], *, size_bytes: int) -> None:
814
+ if "id" in payload:
815
+ if "method" in payload:
816
+ raise BrowserSecurityError("Browser CDP response shape was invalid.")
817
+ request_id = payload.get("id")
818
+ if (
819
+ isinstance(request_id, bool)
820
+ or not isinstance(request_id, int)
821
+ or not 1 <= request_id <= _MAX_RESPONSE_ID
822
+ ):
823
+ raise BrowserSecurityError("Browser CDP response id was invalid.")
824
+ with self._lock:
825
+ pending = self._pending.pop(request_id, None)
826
+ if pending is None:
827
+ raise BrowserSecurityError("Browser CDP response id was unknown or duplicated.")
828
+ pending.response = payload
829
+ pending.event.set()
830
+ return
831
+
832
+ method = payload.get("method")
833
+ params = payload.get("params", {})
834
+ session_id = payload.get("sessionId")
835
+ if not isinstance(method, str) or not _METHOD_RE.fullmatch(method):
836
+ raise BrowserSecurityError("Browser CDP event method was invalid.")
837
+ if not isinstance(params, Mapping):
838
+ raise BrowserSecurityError("Browser CDP event params were invalid.")
839
+ if session_id is not None and (
840
+ not isinstance(session_id, str)
841
+ or not session_id
842
+ or len(session_id) > _MAX_SESSION_ID_CHARS
843
+ or any(ord(char) < 0x20 for char in session_id)
844
+ ):
845
+ raise BrowserSecurityError("Browser CDP event session id was invalid.")
846
+ queued = _QueuedEvent(payload=payload, size_bytes=size_bytes, session_id=session_id)
847
+ if method in {
848
+ "Fetch.authRequired",
849
+ "Fetch.requestPaused",
850
+ "Target.attachedToTarget",
851
+ "Target.detachedFromTarget",
852
+ }:
853
+ with self._lock:
854
+ guarded = session_id is not None and session_id in self._interception_policies
855
+ if method == "Target.detachedFromTarget" and isinstance(params, Mapping):
856
+ detached = params.get("sessionId")
857
+ guarded = guarded or (
858
+ isinstance(detached, str) and detached in self._interception_policies
859
+ )
860
+ if guarded:
861
+ with self._lock:
862
+ if (
863
+ size_bytes > self._max_event_bytes
864
+ or self._interception_event_bytes + size_bytes > self._max_event_bytes
865
+ ):
866
+ raise BrowserSecurityError(
867
+ "Browser request interception byte budget overflowed."
868
+ )
869
+ try:
870
+ self._interception_events.put_nowait(queued)
871
+ except queue.Full as exc:
872
+ raise BrowserSecurityError(
873
+ "Browser request interception queue overflowed."
874
+ ) from exc
875
+ self._interception_event_bytes += size_bytes
876
+ return
877
+ with self._event_changed:
878
+ while self._events and (
879
+ len(self._events) >= self._max_event_count
880
+ or self._event_bytes + size_bytes > self._max_event_bytes
881
+ ):
882
+ removed = self._events.popleft()
883
+ self._event_bytes -= removed.size_bytes
884
+ self._dropped_events += 1
885
+ self._drop_generation += 1
886
+ if size_bytes > self._max_event_bytes:
887
+ self._dropped_events += 1
888
+ self._drop_generation += 1
889
+ else:
890
+ self._events.append(queued)
891
+ self._event_bytes += size_bytes
892
+ self._event_changed.notify_all()
893
+
894
+ def _take_navigation_event(
895
+ self,
896
+ session_id: str,
897
+ *,
898
+ timeout: float,
899
+ ) -> Mapping[str, Any] | None:
900
+ wanted = {"Page.lifecycleEvent"}
901
+ deadline = time.monotonic() + max(0.0, timeout)
902
+ with self._event_changed:
903
+ while True:
904
+ self._raise_if_unavailable_locked()
905
+ for index, item in enumerate(self._events):
906
+ if item.session_id == session_id and item.payload.get("method") in wanted:
907
+ del self._events[index]
908
+ self._event_bytes -= item.size_bytes
909
+ return item.payload
910
+ remaining = deadline - time.monotonic()
911
+ if remaining <= 0:
912
+ return None
913
+ self._event_changed.wait(timeout=min(_WAIT_POLL_SECONDS, remaining))
914
+
915
+ def _next_request_id_locked(self) -> int:
916
+ if self._next_id > _MAX_RESPONSE_ID:
917
+ raise BrowserSecurityError("Browser CDP request id space was exhausted.")
918
+ request_id = self._next_id
919
+ self._next_id += 1
920
+ return request_id
921
+
922
+ def _raise_if_unavailable_locked(self) -> None:
923
+ if self._terminal_error is not None:
924
+ raise self._terminal_error
925
+ if self._closed:
926
+ raise BrowserLaunchError("Browser DevTools WebSocket is closed.")
927
+
928
+ def _fail_transport(self, error: BrowserError) -> None:
929
+ should_close = False
930
+ with self._event_changed:
931
+ if self._terminal_error is None:
932
+ self._terminal_error = error
933
+ should_close = True
934
+ self._closed = True
935
+ pending = tuple(self._pending.values())
936
+ self._pending.clear()
937
+ for item in pending:
938
+ item.error = self._terminal_error
939
+ item.event.set()
940
+ while True:
941
+ try:
942
+ outbound = self._outbound.get_nowait()
943
+ except queue.Empty:
944
+ break
945
+ outbound.error = self._terminal_error
946
+ outbound.sent.set()
947
+ self._event_changed.notify_all()
948
+ if should_close:
949
+ _close_connection_async(self._connection)
950
+
951
+
952
+ def _continue_paused_request(
953
+ transport: WebSocketCdpTransport,
954
+ event: Mapping[str, Any],
955
+ *,
956
+ session_id: str,
957
+ authorize_url: Callable[[str], str],
958
+ deadline: float,
959
+ cancel: CancelCheck,
960
+ ) -> str:
961
+ params = event.get("params")
962
+ if not isinstance(params, Mapping):
963
+ error = BrowserSecurityError("Browser paused request was invalid.")
964
+ transport._fail_transport(error)
965
+ raise error
966
+ request_id = params.get("requestId")
967
+ request = params.get("request")
968
+ if (
969
+ not isinstance(request_id, str)
970
+ or not request_id
971
+ or len(request_id) > 256
972
+ or not isinstance(request, Mapping)
973
+ ):
974
+ error = BrowserSecurityError("Browser paused request was invalid.")
975
+ transport._fail_transport(error)
976
+ raise error
977
+ candidate = request.get("url")
978
+ if not isinstance(candidate, str) or not candidate:
979
+ error = BrowserSecurityError("Browser paused request URL was invalid.")
980
+ transport._fail_transport(error)
981
+ raise error
982
+ response_stage = "responseStatusCode" in params or "responseErrorReason" in params
983
+ try:
984
+ authorized = _authorize(authorize_url, candidate)
985
+ except BaseException as exc:
986
+ try:
987
+ transport.call(
988
+ "Fetch.failRequest",
989
+ {"requestId": request_id, "errorReason": "BlockedByClient"},
990
+ session_id=session_id,
991
+ timeout=_remaining(deadline),
992
+ cancel=cancel,
993
+ )
994
+ except BrowserError:
995
+ transport._fail_transport(
996
+ BrowserSecurityError("Browser rejected request could not be failed safely.")
997
+ )
998
+ if isinstance(exc, BrowserError):
999
+ raise exc
1000
+ raise BrowserSecurityError("Browser request URL policy failed closed.") from exc
1001
+ if response_stage:
1002
+ transport.call(
1003
+ "Fetch.continueResponse",
1004
+ {"requestId": request_id},
1005
+ session_id=session_id,
1006
+ timeout=_remaining(deadline),
1007
+ cancel=cancel,
1008
+ )
1009
+ return "Response"
1010
+ continue_params: dict[str, Any] = {"requestId": request_id}
1011
+ if authorized != candidate:
1012
+ continue_params["url"] = authorized
1013
+ transport.call(
1014
+ "Fetch.continueRequest",
1015
+ continue_params,
1016
+ session_id=session_id,
1017
+ timeout=_remaining(deadline),
1018
+ cancel=cancel,
1019
+ )
1020
+ return "Request"
1021
+
1022
+
1023
+ def _default_connector() -> _Connector:
1024
+ try:
1025
+ from websockets.sync.client import connect
1026
+ except ImportError as exc: # pragma: no cover - installation contract
1027
+ raise BrowserDependencyError(
1028
+ "Managed browser automation requires the websockets package."
1029
+ ) from exc
1030
+ return connect
1031
+
1032
+
1033
+ def _encode_message(payload: Mapping[str, Any]) -> str:
1034
+ try:
1035
+ encoded = json.dumps(
1036
+ payload,
1037
+ ensure_ascii=False,
1038
+ allow_nan=False,
1039
+ separators=(",", ":"),
1040
+ ).encode("utf-8")
1041
+ except (TypeError, ValueError, OverflowError) as exc:
1042
+ raise BrowserValidationError("Browser CDP command was not valid bounded JSON.") from exc
1043
+ if len(encoded) > _MAX_MESSAGE_BYTES:
1044
+ raise BrowserValidationError("Browser CDP command exceeded the 8 MiB limit.")
1045
+ return encoded.decode("utf-8")
1046
+
1047
+
1048
+ def _decode_message(raw: str | bytes) -> tuple[Mapping[str, Any], int]:
1049
+ if isinstance(raw, str):
1050
+ try:
1051
+ encoded = raw.encode("utf-8")
1052
+ except UnicodeError as exc:
1053
+ raise BrowserSecurityError("Browser CDP frame was not valid UTF-8.") from exc
1054
+ elif isinstance(raw, bytes):
1055
+ encoded = raw
1056
+ else:
1057
+ raise BrowserSecurityError("Browser CDP frame type was invalid.")
1058
+ if not encoded or len(encoded) > _MAX_MESSAGE_BYTES:
1059
+ raise BrowserSecurityError("Browser CDP frame exceeded the 8 MiB limit.")
1060
+
1061
+ def reject_constant(value: str) -> None:
1062
+ raise ValueError(f"unsupported JSON constant: {value}")
1063
+
1064
+ def unique_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
1065
+ result: dict[str, Any] = {}
1066
+ for key, value in pairs:
1067
+ if key in result:
1068
+ raise ValueError("duplicate JSON object key")
1069
+ result[key] = value
1070
+ return result
1071
+
1072
+ try:
1073
+ payload = json.loads(
1074
+ encoded,
1075
+ parse_constant=reject_constant,
1076
+ object_pairs_hook=unique_object,
1077
+ )
1078
+ except (UnicodeError, ValueError, RecursionError) as exc:
1079
+ raise BrowserSecurityError("Browser CDP frame was not valid JSON.") from exc
1080
+ if not isinstance(payload, Mapping):
1081
+ raise BrowserSecurityError("Browser CDP message was not an object.")
1082
+ try:
1083
+ decoded_size = len(
1084
+ json.dumps(payload, ensure_ascii=False, allow_nan=False, separators=(",", ":")).encode(
1085
+ "utf-8"
1086
+ )
1087
+ )
1088
+ except (TypeError, ValueError, OverflowError, RecursionError) as exc:
1089
+ raise BrowserSecurityError("Browser CDP decoded message was invalid.") from exc
1090
+ if decoded_size > _MAX_MESSAGE_BYTES:
1091
+ raise BrowserSecurityError("Browser CDP decoded message exceeded the 8 MiB limit.")
1092
+ return payload, decoded_size
1093
+
1094
+
1095
+ def _validated_method(method: str) -> str:
1096
+ if (
1097
+ not isinstance(method, str)
1098
+ or len(method) > _MAX_METHOD_CHARS
1099
+ or not _METHOD_RE.fullmatch(method)
1100
+ ):
1101
+ raise BrowserValidationError("Browser CDP method is invalid.")
1102
+ return method
1103
+
1104
+
1105
+ def _validated_session_id(session_id: str | None) -> str | None:
1106
+ if session_id is None:
1107
+ return None
1108
+ if (
1109
+ not isinstance(session_id, str)
1110
+ or not session_id
1111
+ or len(session_id) > _MAX_SESSION_ID_CHARS
1112
+ or any(ord(char) < 0x20 for char in session_id)
1113
+ ):
1114
+ raise BrowserValidationError("Browser CDP session id is invalid.")
1115
+ return session_id
1116
+
1117
+
1118
+ def _validated_timeout(timeout: float) -> float:
1119
+ if (
1120
+ isinstance(timeout, bool)
1121
+ or not isinstance(timeout, int | float)
1122
+ or not math.isfinite(float(timeout))
1123
+ or not 0.05 <= float(timeout) <= 300.0
1124
+ ):
1125
+ raise BrowserValidationError("Browser CDP timeout is invalid.")
1126
+ return float(timeout)
1127
+
1128
+
1129
+ def _authorize(authorize_url: Callable[[str], str], candidate: str) -> str:
1130
+ try:
1131
+ authorized = authorize_url(candidate)
1132
+ except BrowserError:
1133
+ raise
1134
+ except BaseException as exc: # noqa: BLE001 - policy failure must fail closed
1135
+ raise BrowserSecurityError("Browser request URL policy failed closed.") from exc
1136
+ if not isinstance(authorized, str) or not authorized:
1137
+ raise BrowserSecurityError("Browser request URL policy returned an invalid URL.")
1138
+ return authorized
1139
+
1140
+
1141
+ def _remaining(deadline: float) -> float:
1142
+ remaining = deadline - time.monotonic()
1143
+ if remaining < 0.05:
1144
+ raise BrowserTimeoutError("Browser CDP operation timed out.")
1145
+ return remaining
1146
+
1147
+
1148
+ def _check_cancel(cancel: CancelCheck) -> None:
1149
+ if cancel is not None and cancel():
1150
+ raise BrowserCancelledError("Browser operation was cancelled.")
1151
+
1152
+
1153
+ def _check_cancel_and_deadline(
1154
+ cancel: CancelCheck,
1155
+ deadline: float,
1156
+ transport: WebSocketCdpTransport,
1157
+ ) -> None:
1158
+ if cancel is not None and cancel():
1159
+ error = BrowserCancelledError("Browser navigation was cancelled.")
1160
+ transport._fail_transport(error)
1161
+ raise error
1162
+ if time.monotonic() >= deadline:
1163
+ error = BrowserTimeoutError("Browser navigation timed out.")
1164
+ transport._fail_transport(error)
1165
+ raise error
1166
+
1167
+
1168
+ def _close_connection_async(connection: _WebSocketConnection) -> None:
1169
+ threading.Thread(
1170
+ target=_close_connection_safely,
1171
+ args=(connection,),
1172
+ name="alysis-cdp-close",
1173
+ daemon=True,
1174
+ ).start()
1175
+
1176
+
1177
+ def _close_connection_bounded(connection: _WebSocketConnection, timeout: float) -> None:
1178
+ worker = threading.Thread(
1179
+ target=_close_connection_safely,
1180
+ args=(connection,),
1181
+ name="alysis-cdp-close",
1182
+ daemon=True,
1183
+ )
1184
+ worker.start()
1185
+ worker.join(timeout=timeout)
1186
+
1187
+
1188
+ def _close_connection_safely(connection: _WebSocketConnection) -> None:
1189
+ try:
1190
+ connection.close()
1191
+ except BaseException: # noqa: BLE001 - cleanup must not mask the original failure
1192
+ return