synth-ai 0.2.14__py3-none-any.whl → 0.4.4__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.

Potentially problematic release.


This version of synth-ai might be problematic. Click here for more details.

Files changed (1086) hide show
  1. synth_ai/__init__.py +25 -46
  2. synth_ai/__main__.py +30 -3
  3. synth_ai/cli/__init__.py +98 -72
  4. synth_ai/cli/__main__.py +42 -0
  5. synth_ai/cli/_internal/__init__.py +5 -0
  6. synth_ai/cli/_internal/modal_wrapper.py +31 -0
  7. synth_ai/cli/_internal/storage.py +20 -0
  8. synth_ai/cli/_internal/typer_patch.py +47 -0
  9. synth_ai/cli/_internal/validate_task_app.py +29 -0
  10. synth_ai/cli/agents/__init__.py +17 -0
  11. synth_ai/cli/agents/claude.py +77 -0
  12. synth_ai/cli/agents/codex.py +265 -0
  13. synth_ai/cli/agents/opencode.py +253 -0
  14. synth_ai/cli/commands/__init__.py +18 -0
  15. synth_ai/cli/commands/artifacts/__init__.py +13 -0
  16. synth_ai/cli/commands/artifacts/client.py +119 -0
  17. synth_ai/cli/commands/artifacts/config.py +57 -0
  18. synth_ai/cli/commands/artifacts/core.py +24 -0
  19. synth_ai/cli/commands/artifacts/download.py +188 -0
  20. synth_ai/cli/commands/artifacts/export.py +186 -0
  21. synth_ai/cli/commands/artifacts/list.py +156 -0
  22. synth_ai/cli/commands/artifacts/parsing.py +250 -0
  23. synth_ai/cli/commands/artifacts/show.py +336 -0
  24. synth_ai/cli/commands/demo/__init__.py +3 -0
  25. synth_ai/cli/commands/demo/core.py +153 -0
  26. synth_ai/cli/commands/eval/__init__.py +10 -0
  27. synth_ai/cli/commands/eval/config.py +338 -0
  28. synth_ai/cli/commands/eval/core.py +258 -0
  29. synth_ai/cli/commands/eval/runner.py +704 -0
  30. synth_ai/cli/commands/eval/validation.py +60 -0
  31. synth_ai/cli/commands/filter/__init__.py +12 -0
  32. synth_ai/cli/commands/filter/core.py +424 -0
  33. synth_ai/cli/commands/filter/errors.py +55 -0
  34. synth_ai/cli/commands/filter/validation.py +77 -0
  35. synth_ai/cli/commands/help/__init__.py +185 -0
  36. synth_ai/cli/commands/help/core.py +72 -0
  37. synth_ai/cli/commands/scan/__init__.py +19 -0
  38. synth_ai/cli/commands/scan/cloudflare_scanner.py +403 -0
  39. synth_ai/cli/commands/scan/core.py +344 -0
  40. synth_ai/cli/commands/scan/health_checker.py +242 -0
  41. synth_ai/cli/commands/scan/local_scanner.py +278 -0
  42. synth_ai/cli/commands/scan/models.py +83 -0
  43. synth_ai/cli/commands/smoke/__init__.py +7 -0
  44. synth_ai/cli/commands/smoke/core.py +1428 -0
  45. synth_ai/cli/commands/status/__init__.py +3 -0
  46. synth_ai/cli/commands/status/client.py +91 -0
  47. synth_ai/cli/commands/status/config.py +12 -0
  48. synth_ai/cli/commands/status/errors.py +11 -0
  49. synth_ai/cli/commands/status/subcommands/__init__.py +3 -0
  50. synth_ai/cli/commands/status/subcommands/config.py +13 -0
  51. synth_ai/cli/commands/status/subcommands/files.py +34 -0
  52. synth_ai/cli/commands/status/subcommands/jobs.py +51 -0
  53. synth_ai/cli/commands/status/subcommands/models.py +35 -0
  54. synth_ai/cli/commands/status/subcommands/runs.py +34 -0
  55. synth_ai/cli/commands/status/subcommands/session.py +77 -0
  56. synth_ai/cli/commands/status/subcommands/summary.py +39 -0
  57. synth_ai/cli/commands/status/subcommands/utils.py +41 -0
  58. synth_ai/cli/commands/status/utils.py +23 -0
  59. synth_ai/cli/commands/train/__init__.py +51 -0
  60. synth_ai/cli/commands/train/core.py +22 -0
  61. synth_ai/cli/commands/train/errors.py +117 -0
  62. synth_ai/cli/commands/train/prompt_learning_validation.py +632 -0
  63. synth_ai/cli/commands/train/validation.py +392 -0
  64. synth_ai/cli/commands/train/verifier_schemas.py +200 -0
  65. synth_ai/cli/commands/train/verifier_validation.py +235 -0
  66. synth_ai/cli/demo_apps/__init__.py +10 -0
  67. synth_ai/cli/demo_apps/core/__init__.py +28 -0
  68. synth_ai/cli/demo_apps/core/cli.py +1735 -0
  69. synth_ai/cli/demo_apps/crafter/crafter_fft_4b.toml +55 -0
  70. synth_ai/cli/demo_apps/crafter/grpo_crafter_task_app.py +186 -0
  71. synth_ai/cli/demo_apps/crafter/rl_from_base_qwen4b.toml +74 -0
  72. synth_ai/cli/demo_apps/demo_registry.py +176 -0
  73. synth_ai/cli/demo_apps/demo_task_apps/core.py +440 -0
  74. synth_ai/cli/demo_apps/demo_task_apps/crafter/__init__.py +1 -0
  75. synth_ai/cli/demo_apps/demo_task_apps/crafter/grpo_crafter_task_app.py +185 -0
  76. synth_ai/cli/demo_apps/demo_task_apps/math/config.toml +73 -0
  77. synth_ai/cli/demo_apps/demo_task_apps/math/modal_task_app.py +738 -0
  78. synth_ai/cli/demo_apps/demo_task_apps/math/task_app_entry.py +39 -0
  79. synth_ai/cli/demo_apps/math/__init__.py +1 -0
  80. synth_ai/cli/demo_apps/math/_common.py +16 -0
  81. synth_ai/cli/demo_apps/math/app.py +38 -0
  82. synth_ai/cli/demo_apps/math/config.toml +75 -0
  83. synth_ai/cli/demo_apps/math/deploy_modal.py +54 -0
  84. synth_ai/cli/demo_apps/math/modal_task_app.py +698 -0
  85. synth_ai/cli/demo_apps/math/task_app_entry.py +53 -0
  86. synth_ai/cli/demo_apps/mipro/main.py +271 -0
  87. synth_ai/cli/demo_apps/mipro/task_app.py +911 -0
  88. synth_ai/cli/demo_apps/mipro/train_cfg.toml +92 -0
  89. synth_ai/cli/demos/__init__.py +12 -0
  90. synth_ai/cli/demos/demo.py +32 -0
  91. synth_ai/cli/demos/rl_demo.py +254 -0
  92. synth_ai/cli/deploy.py +216 -0
  93. synth_ai/cli/infra/__init__.py +14 -0
  94. synth_ai/cli/infra/balance.py +216 -0
  95. synth_ai/cli/infra/mcp.py +35 -0
  96. synth_ai/cli/infra/modal_app.py +36 -0
  97. synth_ai/cli/infra/setup.py +69 -0
  98. synth_ai/cli/infra/status.py +16 -0
  99. synth_ai/cli/infra/turso.py +77 -0
  100. synth_ai/cli/lib/__init__.py +10 -0
  101. synth_ai/cli/lib/agents.py +76 -0
  102. synth_ai/cli/lib/apps/modal_app.py +101 -0
  103. synth_ai/cli/lib/apps/task_app.py +642 -0
  104. synth_ai/cli/lib/bin.py +39 -0
  105. synth_ai/cli/lib/env.py +375 -0
  106. synth_ai/cli/lib/errors.py +85 -0
  107. synth_ai/cli/lib/modal.py +315 -0
  108. synth_ai/cli/lib/plotting.py +126 -0
  109. synth_ai/cli/lib/prompt_args.py +39 -0
  110. synth_ai/cli/lib/prompts.py +284 -0
  111. synth_ai/cli/lib/sqld.py +122 -0
  112. synth_ai/cli/lib/task_app_discovery.py +884 -0
  113. synth_ai/cli/lib/task_app_env.py +295 -0
  114. synth_ai/cli/lib/train_cfgs.py +300 -0
  115. synth_ai/cli/lib/tunnel_records.py +207 -0
  116. synth_ai/cli/local/__init__.py +14 -0
  117. synth_ai/cli/local/experiment_queue/__init__.py +72 -0
  118. synth_ai/cli/local/experiment_queue/api_schemas.py +221 -0
  119. synth_ai/cli/local/experiment_queue/celery_app.py +208 -0
  120. synth_ai/cli/local/experiment_queue/config.py +128 -0
  121. synth_ai/cli/local/experiment_queue/config_utils.py +272 -0
  122. synth_ai/cli/local/experiment_queue/database.py +175 -0
  123. synth_ai/cli/local/experiment_queue/dispatcher.py +119 -0
  124. synth_ai/cli/local/experiment_queue/models.py +231 -0
  125. synth_ai/cli/local/experiment_queue/progress_info.py +160 -0
  126. synth_ai/cli/local/experiment_queue/results.py +373 -0
  127. synth_ai/cli/local/experiment_queue/schemas.py +131 -0
  128. synth_ai/cli/local/experiment_queue/service.py +344 -0
  129. synth_ai/cli/local/experiment_queue/status.py +372 -0
  130. synth_ai/cli/local/experiment_queue/status_tracker.py +360 -0
  131. synth_ai/cli/local/experiment_queue/tasks.py +1984 -0
  132. synth_ai/cli/local/experiment_queue/trace_storage.py +65 -0
  133. synth_ai/cli/local/experiment_queue/validation.py +157 -0
  134. synth_ai/cli/local/session/__init__.py +92 -0
  135. synth_ai/cli/local/session/client.py +383 -0
  136. synth_ai/cli/local/session/constants.py +63 -0
  137. synth_ai/cli/local/session/exceptions.py +105 -0
  138. synth_ai/cli/local/session/manager.py +139 -0
  139. synth_ai/cli/local/session/models.py +89 -0
  140. synth_ai/cli/local/session/query.py +110 -0
  141. synth_ai/cli/root.py +30 -6
  142. synth_ai/cli/task_apps/__init__.py +37 -0
  143. synth_ai/cli/task_apps/commands.py +3145 -0
  144. synth_ai/cli/task_apps/deploy.py +7 -0
  145. synth_ai/cli/task_apps/list.py +26 -0
  146. synth_ai/cli/task_apps/main.py +36 -0
  147. synth_ai/cli/task_apps/modal_serve.py +11 -0
  148. synth_ai/cli/task_apps/serve.py +11 -0
  149. synth_ai/cli/training/__init__.py +8 -0
  150. synth_ai/cli/training/train.py +5 -0
  151. synth_ai/cli/training/train_cfg.py +34 -0
  152. synth_ai/cli/training/watch.py +506 -0
  153. synth_ai/cli/turso.py +34 -55
  154. synth_ai/cli/utils/__init__.py +8 -0
  155. synth_ai/cli/utils/experiments.py +235 -0
  156. synth_ai/cli/utils/queue.py +504 -0
  157. synth_ai/cli/utils/recent.py +133 -0
  158. synth_ai/cli/utils/traces.py +164 -0
  159. synth_ai/contracts/__init__.py +67 -0
  160. synth_ai/core/__init__.py +100 -0
  161. synth_ai/core/_utils/__init__.py +54 -0
  162. synth_ai/core/_utils/base_url.py +10 -0
  163. synth_ai/core/_utils/http.py +10 -0
  164. synth_ai/core/_utils/prompts.py +14 -0
  165. synth_ai/core/_utils/task_app_state.py +12 -0
  166. synth_ai/core/_utils/user_config.py +10 -0
  167. synth_ai/core/apps/common.py +116 -0
  168. synth_ai/core/auth.py +95 -0
  169. synth_ai/core/cfgs.py +240 -0
  170. synth_ai/core/config/__init__.py +16 -0
  171. synth_ai/core/config/base.py +168 -0
  172. synth_ai/core/config/resolver.py +89 -0
  173. synth_ai/core/env.py +231 -0
  174. synth_ai/core/errors.py +125 -0
  175. synth_ai/core/http.py +230 -0
  176. synth_ai/core/integrations/__init__.py +11 -0
  177. synth_ai/core/integrations/cloudflare.py +1886 -0
  178. synth_ai/core/integrations/mcp/__init__.py +6 -0
  179. synth_ai/core/integrations/mcp/__main__.py +8 -0
  180. synth_ai/core/integrations/mcp/claude.py +36 -0
  181. synth_ai/core/integrations/mcp/main.py +254 -0
  182. synth_ai/core/integrations/mcp/setup.py +100 -0
  183. synth_ai/core/integrations/modal.py +277 -0
  184. synth_ai/core/json.py +72 -0
  185. synth_ai/core/log_filter.py +99 -0
  186. synth_ai/core/logging.py +82 -0
  187. synth_ai/core/paths.py +107 -0
  188. synth_ai/core/pricing.py +109 -0
  189. synth_ai/core/process.py +233 -0
  190. synth_ai/core/ssl.py +25 -0
  191. synth_ai/core/storage/__init__.py +71 -0
  192. synth_ai/core/task_app_state.py +318 -0
  193. synth_ai/core/telemetry.py +282 -0
  194. synth_ai/core/tracing_v3/__init__.py +99 -0
  195. synth_ai/core/tracing_v3/abstractions.py +348 -0
  196. synth_ai/core/tracing_v3/config.py +229 -0
  197. synth_ai/core/tracing_v3/constants.py +21 -0
  198. synth_ai/core/tracing_v3/db_config.py +182 -0
  199. synth_ai/core/tracing_v3/decorators.py +401 -0
  200. synth_ai/core/tracing_v3/llm_call_record_helpers.py +437 -0
  201. synth_ai/core/tracing_v3/migration_helper.py +119 -0
  202. synth_ai/core/tracing_v3/session_tracer.py +542 -0
  203. synth_ai/core/tracing_v3/storage/base.py +211 -0
  204. synth_ai/core/tracing_v3/storage/config.py +109 -0
  205. synth_ai/core/tracing_v3/storage/factory.py +39 -0
  206. synth_ai/core/tracing_v3/trace_utils.py +326 -0
  207. synth_ai/core/tracing_v3/turso/daemon.py +278 -0
  208. synth_ai/core/tracing_v3/turso/models.py +470 -0
  209. synth_ai/core/tracing_v3/turso/native_manager.py +1385 -0
  210. synth_ai/core/tracing_v3/utils.py +108 -0
  211. synth_ai/core/urls.py +18 -0
  212. synth_ai/core/user_config.py +137 -0
  213. synth_ai/core/uvicorn.py +222 -0
  214. synth_ai/data/__init__.py +83 -0
  215. synth_ai/data/enums.py +122 -0
  216. synth_ai/data/rewards.py +249 -0
  217. synth_ai/data/traces.py +35 -0
  218. synth_ai/products/__init__.py +6 -0
  219. synth_ai/products/graph_evolve/__init__.py +45 -0
  220. synth_ai/products/graph_evolve/client.py +226 -0
  221. synth_ai/products/graph_evolve/config.py +591 -0
  222. synth_ai/products/graph_evolve/converters/__init__.py +42 -0
  223. synth_ai/products/graph_evolve/converters/openai_sft.py +484 -0
  224. synth_ai/products/graph_evolve/examples/hotpotqa/config.toml +109 -0
  225. synth_ai/products/graph_evolve/run.py +222 -0
  226. synth_ai/products/graph_gepa/__init__.py +23 -0
  227. synth_ai/products/graph_gepa/converters/__init__.py +19 -0
  228. synth_ai/products/graph_gepa/converters/openai_sft.py +29 -0
  229. synth_ai/sdk/__init__.py +129 -0
  230. synth_ai/sdk/api/__init__.py +1 -0
  231. synth_ai/sdk/api/eval/__init__.py +33 -0
  232. synth_ai/sdk/api/eval/job.py +732 -0
  233. synth_ai/sdk/api/models/supported.py +514 -0
  234. synth_ai/sdk/api/research_agent/__init__.py +296 -0
  235. synth_ai/sdk/api/train/__init__.py +85 -0
  236. synth_ai/sdk/api/train/builders.py +1076 -0
  237. synth_ai/sdk/api/train/cli.py +2196 -0
  238. synth_ai/sdk/api/train/config_finder.py +267 -0
  239. synth_ai/sdk/api/train/configs/__init__.py +67 -0
  240. synth_ai/sdk/api/train/configs/prompt_learning.py +1800 -0
  241. synth_ai/sdk/api/train/configs/rl.py +436 -0
  242. synth_ai/sdk/api/train/configs/sft.py +263 -0
  243. synth_ai/sdk/api/train/configs/shared.py +81 -0
  244. synth_ai/sdk/api/train/context_learning.py +312 -0
  245. synth_ai/sdk/api/train/env_resolver.py +418 -0
  246. synth_ai/sdk/api/train/graph_validators.py +216 -0
  247. synth_ai/sdk/api/train/graphgen.py +1102 -0
  248. synth_ai/sdk/api/train/graphgen_models.py +873 -0
  249. synth_ai/sdk/api/train/graphgen_validators.py +109 -0
  250. synth_ai/sdk/api/train/local_api.py +10 -0
  251. synth_ai/sdk/api/train/pollers.py +160 -0
  252. synth_ai/sdk/api/train/progress/__init__.py +97 -0
  253. synth_ai/sdk/api/train/progress/dataclasses.py +569 -0
  254. synth_ai/sdk/api/train/progress/events.py +326 -0
  255. synth_ai/sdk/api/train/progress/results.py +428 -0
  256. synth_ai/sdk/api/train/progress/tracker.py +641 -0
  257. synth_ai/sdk/api/train/prompt_learning.py +800 -0
  258. synth_ai/sdk/api/train/rl.py +478 -0
  259. synth_ai/sdk/api/train/sft.py +398 -0
  260. synth_ai/sdk/api/train/summary.py +522 -0
  261. synth_ai/sdk/api/train/supported_algos.py +147 -0
  262. synth_ai/sdk/api/train/task_app.py +351 -0
  263. synth_ai/sdk/api/train/utils.py +279 -0
  264. synth_ai/sdk/api/train/validators.py +2424 -0
  265. synth_ai/sdk/graphs/__init__.py +15 -0
  266. synth_ai/sdk/graphs/completions.py +776 -0
  267. synth_ai/sdk/graphs/verifier_schemas.py +222 -0
  268. synth_ai/sdk/inference/__init__.py +6 -0
  269. synth_ai/sdk/inference/client.py +128 -0
  270. synth_ai/sdk/jobs/__init__.py +16 -0
  271. synth_ai/sdk/jobs/client.py +371 -0
  272. synth_ai/sdk/learning/__init__.py +99 -0
  273. synth_ai/sdk/learning/client.py +240 -0
  274. synth_ai/sdk/learning/context_learning_client.py +531 -0
  275. synth_ai/sdk/learning/context_learning_types.py +294 -0
  276. synth_ai/sdk/learning/ft_client.py +7 -0
  277. synth_ai/sdk/learning/health.py +49 -0
  278. synth_ai/sdk/learning/jobs.py +202 -0
  279. synth_ai/sdk/learning/prompt_extraction.py +334 -0
  280. synth_ai/sdk/learning/prompt_learning_client.py +455 -0
  281. synth_ai/sdk/learning/prompt_learning_types.py +186 -0
  282. synth_ai/sdk/learning/rl/__init__.py +35 -0
  283. synth_ai/sdk/learning/rl/client.py +268 -0
  284. synth_ai/sdk/learning/rl/contracts.py +23 -0
  285. synth_ai/sdk/learning/rl/env_keys.py +166 -0
  286. synth_ai/sdk/learning/rl/secrets.py +13 -0
  287. synth_ai/sdk/learning/sft/client.py +95 -0
  288. synth_ai/sdk/learning/sft/config.py +270 -0
  289. synth_ai/sdk/learning/sft/data.py +698 -0
  290. synth_ai/sdk/learning/validators.py +52 -0
  291. synth_ai/sdk/localapi/__init__.py +40 -0
  292. synth_ai/sdk/localapi/apps/__init__.py +28 -0
  293. synth_ai/sdk/localapi/client.py +10 -0
  294. synth_ai/sdk/localapi/contracts.py +10 -0
  295. synth_ai/sdk/localapi/helpers.py +519 -0
  296. synth_ai/sdk/localapi/rollouts.py +93 -0
  297. synth_ai/sdk/localapi/server.py +29 -0
  298. synth_ai/sdk/localapi/template.py +49 -0
  299. synth_ai/sdk/streaming/__init__.py +35 -0
  300. synth_ai/sdk/streaming/config.py +94 -0
  301. synth_ai/sdk/streaming/handlers.py +1997 -0
  302. synth_ai/sdk/streaming/streamer.py +708 -0
  303. synth_ai/sdk/streaming/types.py +112 -0
  304. synth_ai/sdk/task/__init__.py +164 -0
  305. synth_ai/sdk/task/apps/__init__.py +169 -0
  306. synth_ai/sdk/task/client.py +175 -0
  307. synth_ai/sdk/task/config.py +256 -0
  308. synth_ai/sdk/task/contracts.py +340 -0
  309. synth_ai/sdk/task/datasets.py +108 -0
  310. synth_ai/sdk/task/in_process.py +1200 -0
  311. synth_ai/sdk/task/in_process_runner.py +314 -0
  312. synth_ai/sdk/task/inference_api.py +299 -0
  313. synth_ai/sdk/task/proxy.py +287 -0
  314. synth_ai/sdk/task/rubrics/__init__.py +54 -0
  315. synth_ai/sdk/task/rubrics/loaders.py +156 -0
  316. synth_ai/sdk/task/rubrics/strict.py +148 -0
  317. synth_ai/sdk/task/rubrics.py +219 -0
  318. synth_ai/sdk/task/server.py +640 -0
  319. synth_ai/sdk/task/trace_correlation_helpers.py +557 -0
  320. synth_ai/sdk/task/tracing_utils.py +95 -0
  321. synth_ai/sdk/task/validators.py +441 -0
  322. synth_ai/sdk/training/__init__.py +93 -0
  323. synth_ai/sdk/tunnels/__init__.py +118 -0
  324. synth_ai/sdk/tunnels/cleanup.py +83 -0
  325. synth_ai/sdk/tunnels/ports.py +120 -0
  326. synth_ai/sdk/tunnels/tunneled_api.py +363 -0
  327. synth_ai/utils/__init__.py +213 -0
  328. synth_ai-0.4.4.dist-info/METADATA +262 -0
  329. synth_ai-0.4.4.dist-info/RECORD +369 -0
  330. synth_ai-0.4.4.dist-info/top_level.txt +1 -0
  331. examples/__init__.py +0 -16
  332. examples/analyze_semantic_words.sh +0 -17
  333. examples/crafter_debug_render.py +0 -186
  334. examples/dev/qwen3_32b_qlora_4xh100.toml +0 -40
  335. examples/multi_step/configs/README_verilog_rl.md +0 -77
  336. examples/multi_step/configs/VERILOG_REWARDS.md +0 -90
  337. examples/multi_step/configs/VERILOG_RL_CHECKLIST.md +0 -183
  338. examples/multi_step/configs/crafter_eval_synth_qwen4b.toml +0 -35
  339. examples/multi_step/configs/crafter_eval_text_only_groq_qwen32b.toml +0 -36
  340. examples/multi_step/configs/crafter_rl_outcome.toml +0 -74
  341. examples/multi_step/configs/crafter_rl_stepwise_hosted_judge.toml +0 -187
  342. examples/multi_step/configs/crafter_rl_stepwise_shaped.toml +0 -83
  343. examples/multi_step/configs/crafter_rl_stepwise_simple.toml +0 -78
  344. examples/multi_step/configs/crafter_synth_backend.md +0 -40
  345. examples/multi_step/configs/verilog_eval_groq_qwen32b.toml +0 -31
  346. examples/multi_step/configs/verilog_eval_synth_qwen8b.toml +0 -33
  347. examples/multi_step/configs/verilog_rl_lora.toml +0 -190
  348. examples/multi_step/crafter_rl_lora.md +0 -70
  349. examples/multi_step/judges/crafter_backend_judge.py +0 -220
  350. examples/multi_step/judges/verilog_backend_judge.py +0 -234
  351. examples/multi_step/readme.md +0 -48
  352. examples/multi_step/sse_metrics_streaming_notes.md +0 -357
  353. examples/multi_step/task_app_config_notes.md +0 -494
  354. examples/multi_step/verilog_rl_lora.md +0 -218
  355. examples/qwen_coder/README.md +0 -102
  356. examples/qwen_coder/_shared.py +0 -113
  357. examples/qwen_coder/configs/coder_lora_30b.toml +0 -61
  358. examples/qwen_coder/configs/coder_lora_4b.toml +0 -57
  359. examples/qwen_coder/configs/coder_lora_small.toml +0 -58
  360. examples/qwen_coder/generate_dataset.py +0 -98
  361. examples/qwen_coder/infer_ft_smoke.py +0 -65
  362. examples/qwen_coder/infer_prod_proxy.py +0 -73
  363. examples/qwen_coder/infer_via_synth.py +0 -87
  364. examples/qwen_coder/scripts/infer_coder.sh +0 -19
  365. examples/qwen_coder/scripts/train_coder_30b.sh +0 -22
  366. examples/qwen_coder/sft_full_17b.py +0 -103
  367. examples/qwen_coder/sft_lora_30b.py +0 -110
  368. examples/qwen_coder/subset_jsonl.py +0 -39
  369. examples/qwen_coder/todos.md +0 -38
  370. examples/qwen_coder/validate_jsonl.py +0 -60
  371. examples/rl/README.md +0 -169
  372. examples/rl/download_dataset.py +0 -80
  373. examples/run_crafter_demo.sh +0 -10
  374. examples/sft/README.md +0 -139
  375. examples/sft/configs/crafter_fft_qwen0p6b.toml +0 -44
  376. examples/sft/configs/crafter_lora_qwen0p6b.toml +0 -45
  377. examples/sft/evaluate.py +0 -119
  378. examples/sft/export_dataset.py +0 -117
  379. examples/sft/generate_traces.py +0 -164
  380. examples/swe/__init__.py +0 -12
  381. examples/swe/task_app/README.md +0 -105
  382. examples/swe/task_app/__init__.py +0 -2
  383. examples/swe/task_app/grpo_swe_mini.py +0 -601
  384. examples/swe/task_app/grpo_swe_mini_task_app.py +0 -136
  385. examples/swe/task_app/hosted/README.md +0 -173
  386. examples/swe/task_app/hosted/__init__.py +0 -5
  387. examples/swe/task_app/hosted/branching.py +0 -143
  388. examples/swe/task_app/hosted/environment_routes.py +0 -1289
  389. examples/swe/task_app/hosted/envs/__init__.py +0 -1
  390. examples/swe/task_app/hosted/envs/crafter/__init__.py +0 -6
  391. examples/swe/task_app/hosted/envs/crafter/app.py +0 -1
  392. examples/swe/task_app/hosted/envs/crafter/environment.py +0 -522
  393. examples/swe/task_app/hosted/envs/crafter/policy.py +0 -478
  394. examples/swe/task_app/hosted/envs/crafter/react_agent.py +0 -108
  395. examples/swe/task_app/hosted/envs/crafter/shared.py +0 -305
  396. examples/swe/task_app/hosted/envs/crafter/tools.py +0 -47
  397. examples/swe/task_app/hosted/envs/mini_swe/__init__.py +0 -8
  398. examples/swe/task_app/hosted/envs/mini_swe/environment.py +0 -1164
  399. examples/swe/task_app/hosted/envs/mini_swe/policy.py +0 -355
  400. examples/swe/task_app/hosted/envs/mini_swe/shared.py +0 -83
  401. examples/swe/task_app/hosted/envs/mini_swe/tools.py +0 -96
  402. examples/swe/task_app/hosted/hosted_app.py +0 -204
  403. examples/swe/task_app/hosted/inference/__init__.py +0 -5
  404. examples/swe/task_app/hosted/inference/openai_client.py +0 -618
  405. examples/swe/task_app/hosted/main.py +0 -100
  406. examples/swe/task_app/hosted/policy_routes.py +0 -1079
  407. examples/swe/task_app/hosted/registry.py +0 -195
  408. examples/swe/task_app/hosted/rollout.py +0 -1911
  409. examples/swe/task_app/hosted/storage/__init__.py +0 -5
  410. examples/swe/task_app/hosted/storage/volume.py +0 -211
  411. examples/swe/task_app/hosted/test_agents.py +0 -161
  412. examples/swe/task_app/hosted/test_service.py +0 -136
  413. examples/swe/task_app/hosted/utils.py +0 -62
  414. examples/task_apps/IMAGE_ONLY_EVAL_QUICKSTART.md +0 -258
  415. examples/task_apps/TESTING.md +0 -275
  416. examples/task_apps/crafter/CREATE_SFT_DATASET.md +0 -273
  417. examples/task_apps/crafter/EVAL_IMAGE_ONLY_RESULTS.md +0 -152
  418. examples/task_apps/crafter/FILTER_COMMAND_STATUS.md +0 -174
  419. examples/task_apps/crafter/FILTER_COMMAND_SUCCESS.md +0 -268
  420. examples/task_apps/crafter/QUERY_EXAMPLES.md +0 -203
  421. examples/task_apps/crafter/README_IMAGE_ONLY_EVAL.md +0 -316
  422. examples/task_apps/crafter/__init__.py +0 -0
  423. examples/task_apps/crafter/eval_image_only_gpt4o.toml +0 -28
  424. examples/task_apps/crafter/eval_text_only_groq_llama.toml +0 -36
  425. examples/task_apps/crafter/filter_sft_dataset.toml +0 -16
  426. examples/task_apps/crafter/task_app/README.md +0 -42
  427. examples/task_apps/crafter/task_app/__init__.py +0 -5
  428. examples/task_apps/crafter/task_app/grpo_crafter.py +0 -973
  429. examples/task_apps/crafter/task_app/grpo_crafter_task_app.py +0 -146
  430. examples/task_apps/crafter/task_app/synth_envs_hosted/README.md +0 -173
  431. examples/task_apps/crafter/task_app/synth_envs_hosted/__init__.py +0 -5
  432. examples/task_apps/crafter/task_app/synth_envs_hosted/branching.py +0 -143
  433. examples/task_apps/crafter/task_app/synth_envs_hosted/environment_routes.py +0 -1226
  434. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/__init__.py +0 -1
  435. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/__init__.py +0 -6
  436. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/app.py +0 -1
  437. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/environment.py +0 -532
  438. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/policy.py +0 -547
  439. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/react_agent.py +0 -123
  440. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/shared.py +0 -305
  441. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/tools.py +0 -47
  442. examples/task_apps/crafter/task_app/synth_envs_hosted/hosted_app.py +0 -204
  443. examples/task_apps/crafter/task_app/synth_envs_hosted/inference/__init__.py +0 -5
  444. examples/task_apps/crafter/task_app/synth_envs_hosted/inference/openai_client.py +0 -704
  445. examples/task_apps/crafter/task_app/synth_envs_hosted/main.py +0 -100
  446. examples/task_apps/crafter/task_app/synth_envs_hosted/policy_routes.py +0 -1152
  447. examples/task_apps/crafter/task_app/synth_envs_hosted/registry.py +0 -195
  448. examples/task_apps/crafter/task_app/synth_envs_hosted/rollout.py +0 -2160
  449. examples/task_apps/crafter/task_app/synth_envs_hosted/storage/__init__.py +0 -5
  450. examples/task_apps/crafter/task_app/synth_envs_hosted/storage/volume.py +0 -211
  451. examples/task_apps/crafter/task_app/synth_envs_hosted/test_agents.py +0 -161
  452. examples/task_apps/crafter/task_app/synth_envs_hosted/test_service.py +0 -136
  453. examples/task_apps/crafter/task_app/synth_envs_hosted/utils.py +0 -218
  454. examples/task_apps/dev/pokemon_emerald/__init__.py +0 -2
  455. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/README.md +0 -811
  456. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/__init__.py +0 -120
  457. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/action.py +0 -160
  458. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/memory.py +0 -155
  459. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/perception.py +0 -69
  460. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/planning.py +0 -96
  461. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/simple.py +0 -1502
  462. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/system_prompt.py +0 -4
  463. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/grab_map.py +0 -68
  464. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/manual.py +0 -216
  465. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/__init__.py +0 -35
  466. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/emerald_utils.py +0 -631
  467. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/emulator.py +0 -1544
  468. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/enums.py +0 -1428
  469. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/memory_reader.py +0 -4848
  470. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/types.py +0 -41
  471. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/utils.py +0 -298
  472. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pyproject.toml +0 -95
  473. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/run.py +0 -204
  474. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server/__init__.py +0 -0
  475. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server/app.py +0 -2152
  476. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server/client.py +0 -429
  477. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server/frame_server.py +0 -155
  478. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/README.md +0 -78
  479. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/__init__.py +0 -0
  480. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/run_tests.py +0 -122
  481. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_agent_direct.py +0 -76
  482. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_agent_prompts.py +0 -413
  483. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_battle_state_formatting.py +0 -204
  484. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_dialogue_detection.py +0 -133
  485. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_dialogue_detection_comprehensive.py +0 -229
  486. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_direct_agent_emulator.py +0 -300
  487. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_fps_adjustment_pytest.py +0 -205
  488. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_house_to_outside_direct.py +0 -200
  489. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_house_to_outside_transition.py +0 -284
  490. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_map_ground_truth_comparison.py +0 -468
  491. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_memory_map.py +0 -575
  492. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_server_map_validation.py +0 -311
  493. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_torchic_state.py +0 -259
  494. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/__init__.py +0 -0
  495. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/anticheat.py +0 -372
  496. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/checkpoint.py +0 -296
  497. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/error_handler.py +0 -275
  498. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/get_local_ip.py +0 -22
  499. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/helpers.py +0 -44
  500. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/llm_logger.py +0 -514
  501. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_formatter.py +0 -415
  502. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_stitcher.py +0 -1763
  503. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_stitcher_singleton.py +0 -33
  504. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_trimmer.py +0 -106
  505. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_visualizer.py +0 -334
  506. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/ocr_dialogue.py +0 -1020
  507. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/recording.py +0 -188
  508. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/state_formatter.py +0 -1481
  509. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/vlm.py +0 -862
  510. examples/task_apps/dev/pokemon_emerald/modal_app.py +0 -114
  511. examples/task_apps/dev/pokemon_emerald/task_app/README.md +0 -81
  512. examples/task_apps/dev/pokemon_emerald/task_app/__init__.py +0 -6
  513. examples/task_apps/dev/pokemon_emerald/task_app/pokemon_emerald.py +0 -685
  514. examples/task_apps/enron/__init__.py +0 -1
  515. examples/task_apps/enron/eval_groq_qwen32.toml +0 -16
  516. examples/task_apps/enron/filter_sft.toml +0 -5
  517. examples/task_apps/enron/task_app/README.md +0 -14
  518. examples/task_apps/enron/task_app/__init__.py +0 -1
  519. examples/task_apps/enron/task_app/grpo_enron.py +0 -906
  520. examples/task_apps/enron/task_app/grpo_enron_task_app.py +0 -146
  521. examples/task_apps/enron/tests/__init__.py +0 -4
  522. examples/task_apps/enron/tests/conftest.py +0 -115
  523. examples/task_apps/enron/tests/integration/__init__.py +0 -4
  524. examples/task_apps/enron/tests/integration/test_enron_eval.py +0 -179
  525. examples/task_apps/enron/tests/integration/test_enron_rollout.py +0 -135
  526. examples/task_apps/enron/tests/unit/__init__.py +0 -4
  527. examples/task_apps/enron/tests/unit/test_enron_environment.py +0 -126
  528. examples/task_apps/math/README.md +0 -22
  529. examples/task_apps/math/__init__.py +0 -0
  530. examples/task_apps/math/math_single_step.py +0 -1000
  531. examples/task_apps/math/math_task_app.py +0 -115
  532. examples/task_apps/pokemon_battle/__init__.py +0 -2
  533. examples/task_apps/pokemon_battle/modal_app.py +0 -104
  534. examples/task_apps/pokemon_battle/task_app/README.md +0 -68
  535. examples/task_apps/pokemon_battle/task_app/__init__.py +0 -6
  536. examples/task_apps/pokemon_battle/task_app/pokemon_showdown.py +0 -932
  537. examples/task_apps/pokemon_red/EVAL_IMAGE_ONLY_COMPLETE.md +0 -283
  538. examples/task_apps/pokemon_red/EVAL_IMAGE_ONLY_STATUS.md +0 -155
  539. examples/task_apps/pokemon_red/README.md +0 -357
  540. examples/task_apps/pokemon_red/README_IMAGE_ONLY_EVAL.md +0 -415
  541. examples/task_apps/pokemon_red/__init__.py +0 -3
  542. examples/task_apps/pokemon_red/eval_image_only_gpt4o.toml +0 -29
  543. examples/task_apps/pokemon_red/eval_pokemon_red_policy.py +0 -225
  544. examples/task_apps/pokemon_red/pallet_town_rl_config.toml +0 -75
  545. examples/task_apps/pokemon_red/task_app.py +0 -799
  546. examples/task_apps/pokemon_red/test_pallet_town_rewards.py +0 -193
  547. examples/task_apps/sokoban/README.md +0 -307
  548. examples/task_apps/sokoban/__init__.py +0 -3
  549. examples/task_apps/sokoban/eval_groq_qwen32.toml +0 -16
  550. examples/task_apps/sokoban/eval_openai_gpt5.toml +0 -16
  551. examples/task_apps/sokoban/filter_sft.toml +0 -5
  552. examples/task_apps/sokoban/task_app.py +0 -1058
  553. examples/task_apps/sokoban/tests/__init__.py +0 -4
  554. examples/task_apps/sokoban/tests/conftest.py +0 -113
  555. examples/task_apps/sokoban/tests/integration/__init__.py +0 -4
  556. examples/task_apps/sokoban/tests/integration/test_sokoban_eval.py +0 -57
  557. examples/task_apps/sokoban/tests/integration/test_sokoban_rollout.py +0 -198
  558. examples/task_apps/sokoban/tests/unit/__init__.py +0 -4
  559. examples/task_apps/sokoban/tests/unit/test_sokoban_environment.py +0 -114
  560. examples/task_apps/verilog/__init__.py +0 -1
  561. examples/task_apps/verilog/eval_groq_qwen32b.toml +0 -24
  562. examples/task_apps/verilog/filter_sft.toml +0 -5
  563. examples/task_apps/verilog/task_app/README.md +0 -12
  564. examples/task_apps/verilog/task_app/__init__.py +0 -1
  565. examples/task_apps/verilog/task_app/grpo_verilog.py +0 -1166
  566. examples/task_apps/verilog/task_app/grpo_verilog_task_app.py +0 -145
  567. examples/task_apps/verilog/tests/__init__.py +0 -4
  568. examples/task_apps/verilog/tests/conftest.py +0 -115
  569. examples/task_apps/verilog/tests/integration/__init__.py +0 -4
  570. examples/task_apps/verilog/tests/integration/test_verilog_eval.py +0 -181
  571. examples/task_apps/verilog/tests/integration/test_verilog_rollout.py +0 -55
  572. examples/task_apps/verilog/tests/unit/__init__.py +0 -4
  573. examples/task_apps/verilog/tests/unit/test_verilog_scoring.py +0 -118
  574. examples/vlm/PROPOSAL.md +0 -53
  575. examples/vlm/README.md +0 -68
  576. examples/vlm/configs/crafter_vlm_gpt4o.toml +0 -44
  577. examples/vlm/crafter_image_only_agent.py +0 -207
  578. examples/vlm/crafter_openai_vlm_agent.py +0 -277
  579. examples/vlm/filter_image_rows.py +0 -63
  580. examples/vlm/run_crafter_vlm_benchmark.py +0 -316
  581. examples/warming_up_to_rl/analyze_trace_db.py +0 -422
  582. examples/warming_up_to_rl/configs/crafter_fft.toml +0 -48
  583. examples/warming_up_to_rl/configs/crafter_fft_4b.toml +0 -54
  584. examples/warming_up_to_rl/configs/eval_fft_qwen4b.toml +0 -20
  585. examples/warming_up_to_rl/configs/eval_groq_qwen32b.toml +0 -13
  586. examples/warming_up_to_rl/configs/eval_modal_qwen4b.toml +0 -23
  587. examples/warming_up_to_rl/configs/eval_stepwise_complex.toml +0 -35
  588. examples/warming_up_to_rl/configs/eval_stepwise_consistent.toml +0 -26
  589. examples/warming_up_to_rl/configs/eval_stepwise_per_achievement.toml +0 -36
  590. examples/warming_up_to_rl/configs/eval_stepwise_simple.toml +0 -32
  591. examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +0 -83
  592. examples/warming_up_to_rl/configs/rl_from_ft.toml +0 -56
  593. examples/warming_up_to_rl/export_trace_sft.py +0 -723
  594. examples/warming_up_to_rl/groq_test.py +0 -97
  595. examples/warming_up_to_rl/manage_secrets.py +0 -131
  596. examples/warming_up_to_rl/old/event_rewards.md +0 -234
  597. examples/warming_up_to_rl/old/notes.md +0 -73
  598. examples/warming_up_to_rl/readme.md +0 -179
  599. examples/warming_up_to_rl/run_eval.py +0 -736
  600. examples/warming_up_to_rl/run_fft_and_save.py +0 -380
  601. examples/warming_up_to_rl/run_local_rollout.py +0 -239
  602. examples/warming_up_to_rl/run_local_rollout_modal.py +0 -248
  603. examples/warming_up_to_rl/run_local_rollout_parallel.py +0 -405
  604. examples/warming_up_to_rl/run_local_rollout_traced.py +0 -477
  605. examples/warming_up_to_rl/run_rl_and_save.py +0 -124
  606. examples/warming_up_to_rl/run_rollout_remote.py +0 -156
  607. examples/workflows/__init__.py +0 -0
  608. examples/workflows/math_rl/__init__.py +0 -0
  609. examples/workflows/math_rl/configs/eval_base_qwen.toml +0 -15
  610. examples/workflows/math_rl/configs/eval_rl_qwen.toml +0 -11
  611. examples/workflows/math_rl/configs/rl_from_base_qwen.toml +0 -35
  612. examples/workflows/math_rl/configs/rl_from_base_qwen17.toml +0 -74
  613. examples/workflows/math_rl/configs/rl_from_ft_qwen.toml +0 -35
  614. examples/workflows/math_rl/download_dataset.py +0 -80
  615. examples/workflows/math_rl/run_eval.py +0 -436
  616. examples/workflows/math_rl/run_rl_and_save.py +0 -111
  617. synth_ai/api/models/supported.py +0 -377
  618. synth_ai/api/train/__init__.py +0 -5
  619. synth_ai/api/train/builders.py +0 -351
  620. synth_ai/api/train/cli.py +0 -635
  621. synth_ai/api/train/config_finder.py +0 -228
  622. synth_ai/api/train/configs/__init__.py +0 -44
  623. synth_ai/api/train/configs/rl.py +0 -134
  624. synth_ai/api/train/configs/sft.py +0 -95
  625. synth_ai/api/train/configs/shared.py +0 -24
  626. synth_ai/api/train/env_resolver.py +0 -349
  627. synth_ai/api/train/pollers.py +0 -75
  628. synth_ai/api/train/supported_algos.py +0 -147
  629. synth_ai/api/train/task_app.py +0 -195
  630. synth_ai/api/train/utils.py +0 -225
  631. synth_ai/cli/_modal_wrapper.py +0 -29
  632. synth_ai/cli/_storage.py +0 -20
  633. synth_ai/cli/_typer_patch.py +0 -49
  634. synth_ai/cli/_validate_task_app.py +0 -11
  635. synth_ai/cli/balance.py +0 -216
  636. synth_ai/cli/calc.py +0 -84
  637. synth_ai/cli/demo.py +0 -165
  638. synth_ai/cli/legacy_root_backup.py +0 -468
  639. synth_ai/cli/man.py +0 -106
  640. synth_ai/cli/recent.py +0 -132
  641. synth_ai/cli/rl_demo.py +0 -254
  642. synth_ai/cli/status.py +0 -134
  643. synth_ai/cli/task_apps.py +0 -4523
  644. synth_ai/cli/traces.py +0 -164
  645. synth_ai/cli/tui.py +0 -57
  646. synth_ai/cli/watch.py +0 -506
  647. synth_ai/compound/cais.py +0 -0
  648. synth_ai/config/base_url.py +0 -107
  649. synth_ai/core/experiment.py +0 -13
  650. synth_ai/core/system.py +0 -15
  651. synth_ai/demo_registry.py +0 -295
  652. synth_ai/demos/core/__init__.py +0 -1
  653. synth_ai/demos/core/cli.py +0 -1718
  654. synth_ai/demos/demo_task_apps/core.py +0 -440
  655. synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +0 -184
  656. synth_ai/demos/demo_task_apps/math/config.toml +0 -74
  657. synth_ai/demos/demo_task_apps/math/deploy_task_app.sh +0 -22
  658. synth_ai/demos/demo_task_apps/math/modal_task_app.py +0 -739
  659. synth_ai/demos/demo_task_apps/math/task_app_entry.py +0 -37
  660. synth_ai/environments/__init__.py +0 -31
  661. synth_ai/environments/environment/__init__.py +0 -1
  662. synth_ai/environments/environment/artifacts/__init__.py +0 -1
  663. synth_ai/environments/environment/artifacts/base.py +0 -52
  664. synth_ai/environments/environment/core.py +0 -67
  665. synth_ai/environments/environment/db/__init__.py +0 -1
  666. synth_ai/environments/environment/db/sqlite.py +0 -45
  667. synth_ai/environments/environment/registry.py +0 -233
  668. synth_ai/environments/environment/resources/sqlite.py +0 -45
  669. synth_ai/environments/environment/results.py +0 -1
  670. synth_ai/environments/environment/rewards/__init__.py +0 -1
  671. synth_ai/environments/environment/rewards/core.py +0 -29
  672. synth_ai/environments/environment/shared_engine.py +0 -26
  673. synth_ai/environments/environment/tools/__init__.py +0 -200
  674. synth_ai/environments/examples/__init__.py +0 -1
  675. synth_ai/environments/examples/bandit/__init__.py +0 -33
  676. synth_ai/environments/examples/bandit/engine.py +0 -302
  677. synth_ai/environments/examples/bandit/environment.py +0 -194
  678. synth_ai/environments/examples/bandit/taskset.py +0 -200
  679. synth_ai/environments/examples/crafter_classic/__init__.py +0 -8
  680. synth_ai/environments/examples/crafter_classic/agent_demos/analyze_semantic_words_markdown.py +0 -250
  681. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_comprehensive_evaluation.py +0 -59
  682. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_evaluation_browser.py +0 -152
  683. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_evaluation_config.toml +0 -24
  684. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_evaluation_framework.py +0 -1194
  685. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/crafter_synth_config.toml +0 -56
  686. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/filter_config_modal.toml +0 -32
  687. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/filter_traces_sft_turso.py +0 -738
  688. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/kick_off_ft_modal.py +0 -384
  689. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_action_results.py +0 -53
  690. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_agent_actions.py +0 -178
  691. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_latest_run.py +0 -222
  692. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_lm_traces.py +0 -183
  693. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_no_rewards.py +0 -210
  694. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_trace_issue.py +0 -206
  695. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/check_db_schema.py +0 -49
  696. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/check_latest_results.py +0 -64
  697. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/debug_agent_responses.py +0 -88
  698. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/quick_trace_check.py +0 -77
  699. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/compare_experiments.py +0 -324
  700. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/filter_traces_sft_turso.py +0 -580
  701. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/kick_off_ft_oai.py +0 -362
  702. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/multi_model_config.toml +0 -49
  703. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/analyze_enhanced_hooks.py +0 -332
  704. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/analyze_hook_events.py +0 -97
  705. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/analyze_hook_results.py +0 -217
  706. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/check_hook_storage.py +0 -87
  707. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/check_seeds.py +0 -88
  708. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/compare_seed_performance.py +0 -195
  709. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/custom_eval_pipelines.py +0 -400
  710. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/plot_hook_frequency.py +0 -195
  711. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/seed_analysis_summary.py +0 -56
  712. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/run_rollouts_for_models_and_compare_v3.py +0 -858
  713. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_quick_evaluation.py +0 -52
  714. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_react_agent.py +0 -874
  715. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_trace_evaluation.py +0 -1412
  716. synth_ai/environments/examples/crafter_classic/agent_demos/example_v3_usage.py +0 -216
  717. synth_ai/environments/examples/crafter_classic/agent_demos/old/compare_traces.py +0 -296
  718. synth_ai/environments/examples/crafter_classic/agent_demos/old/crafter_comprehensive_evaluation.py +0 -58
  719. synth_ai/environments/examples/crafter_classic/agent_demos/old/crafter_env_serialization.py +0 -464
  720. synth_ai/environments/examples/crafter_classic/agent_demos/old/crafter_evaluation_browser.py +0 -152
  721. synth_ai/environments/examples/crafter_classic/agent_demos/old/crafter_quick_evaluation.py +0 -51
  722. synth_ai/environments/examples/crafter_classic/agent_demos/old/crafter_trace_evaluation.py +0 -1412
  723. synth_ai/environments/examples/crafter_classic/agent_demos/old/debug_player_loss.py +0 -112
  724. synth_ai/environments/examples/crafter_classic/agent_demos/old/diagnose_service.py +0 -203
  725. synth_ai/environments/examples/crafter_classic/agent_demos/old/diagnose_slowness.py +0 -305
  726. synth_ai/environments/examples/crafter_classic/agent_demos/old/eval_by_difficulty.py +0 -126
  727. synth_ai/environments/examples/crafter_classic/agent_demos/old/eval_example.py +0 -94
  728. synth_ai/environments/examples/crafter_classic/agent_demos/old/explore_saved_states.py +0 -142
  729. synth_ai/environments/examples/crafter_classic/agent_demos/old/filter_traces_sft.py +0 -26
  730. synth_ai/environments/examples/crafter_classic/agent_demos/old/filter_traces_sft_OLD.py +0 -984
  731. synth_ai/environments/examples/crafter_classic/agent_demos/old/generate_ft_data_gemini.py +0 -724
  732. synth_ai/environments/examples/crafter_classic/agent_demos/old/generate_ft_data_modal.py +0 -386
  733. synth_ai/environments/examples/crafter_classic/agent_demos/old/generate_ft_metadata.py +0 -205
  734. synth_ai/environments/examples/crafter_classic/agent_demos/old/kick_off_ft_gemini.py +0 -150
  735. synth_ai/environments/examples/crafter_classic/agent_demos/old/kick_off_ft_modal.py +0 -283
  736. synth_ai/environments/examples/crafter_classic/agent_demos/old/prepare_vertex_ft.py +0 -280
  737. synth_ai/environments/examples/crafter_classic/agent_demos/old/profile_env_slowness.py +0 -456
  738. synth_ai/environments/examples/crafter_classic/agent_demos/old/replicate_issue.py +0 -166
  739. synth_ai/environments/examples/crafter_classic/agent_demos/old/run_and_eval.py +0 -102
  740. synth_ai/environments/examples/crafter_classic/agent_demos/old/run_comparison.py +0 -128
  741. synth_ai/environments/examples/crafter_classic/agent_demos/old/run_qwen_rollouts.py +0 -655
  742. synth_ai/environments/examples/crafter_classic/agent_demos/old/trace_eval_OLD.py +0 -202
  743. synth_ai/environments/examples/crafter_classic/agent_demos/old/validate_openai_format.py +0 -166
  744. synth_ai/environments/examples/crafter_classic/config_logging.py +0 -111
  745. synth_ai/environments/examples/crafter_classic/debug_translation.py +0 -0
  746. synth_ai/environments/examples/crafter_classic/engine.py +0 -579
  747. synth_ai/environments/examples/crafter_classic/engine_deterministic_patch.py +0 -64
  748. synth_ai/environments/examples/crafter_classic/engine_helpers/action_map.py +0 -6
  749. synth_ai/environments/examples/crafter_classic/engine_helpers/serialization.py +0 -75
  750. synth_ai/environments/examples/crafter_classic/engine_serialization_patch_v3.py +0 -267
  751. synth_ai/environments/examples/crafter_classic/environment.py +0 -495
  752. synth_ai/environments/examples/crafter_classic/taskset.py +0 -233
  753. synth_ai/environments/examples/crafter_classic/trace_hooks_v3.py +0 -228
  754. synth_ai/environments/examples/crafter_classic/world_config_patch_simple.py +0 -299
  755. synth_ai/environments/examples/crafter_custom/__init__.py +0 -4
  756. synth_ai/environments/examples/crafter_custom/agent_demos/__init__.py +0 -1
  757. synth_ai/environments/examples/crafter_custom/agent_demos/trace_eval.py +0 -202
  758. synth_ai/environments/examples/crafter_custom/crafter/__init__.py +0 -7
  759. synth_ai/environments/examples/crafter_custom/crafter/config.py +0 -182
  760. synth_ai/environments/examples/crafter_custom/crafter/constants.py +0 -8
  761. synth_ai/environments/examples/crafter_custom/crafter/engine.py +0 -269
  762. synth_ai/environments/examples/crafter_custom/crafter/env.py +0 -262
  763. synth_ai/environments/examples/crafter_custom/crafter/objects.py +0 -417
  764. synth_ai/environments/examples/crafter_custom/crafter/recorder.py +0 -187
  765. synth_ai/environments/examples/crafter_custom/crafter/worldgen.py +0 -118
  766. synth_ai/environments/examples/crafter_custom/dataset_builder.py +0 -373
  767. synth_ai/environments/examples/crafter_custom/environment.py +0 -312
  768. synth_ai/environments/examples/crafter_custom/old/analyze_diamond_issue.py +0 -159
  769. synth_ai/environments/examples/crafter_custom/old/analyze_diamond_spawning.py +0 -158
  770. synth_ai/environments/examples/crafter_custom/old/compare_worlds.py +0 -71
  771. synth_ai/environments/examples/crafter_custom/old/dataset_stats.py +0 -105
  772. synth_ai/environments/examples/crafter_custom/old/diamond_spawning_summary.py +0 -119
  773. synth_ai/environments/examples/crafter_custom/old/example_dataset_usage.py +0 -52
  774. synth_ai/environments/examples/crafter_custom/run_dataset.py +0 -305
  775. synth_ai/environments/examples/enron/art_helpers/email_search_tools.py +0 -156
  776. synth_ai/environments/examples/enron/art_helpers/local_email_db.py +0 -281
  777. synth_ai/environments/examples/enron/art_helpers/types_enron.py +0 -25
  778. synth_ai/environments/examples/enron/engine.py +0 -300
  779. synth_ai/environments/examples/enron/environment.py +0 -234
  780. synth_ai/environments/examples/enron/taskset.py +0 -112
  781. synth_ai/environments/examples/enron/units/keyword_stats.py +0 -112
  782. synth_ai/environments/examples/minigrid/__init__.py +0 -48
  783. synth_ai/environments/examples/minigrid/agent_demos/minigrid_evaluation_framework.py +0 -1188
  784. synth_ai/environments/examples/minigrid/agent_demos/minigrid_quick_evaluation.py +0 -48
  785. synth_ai/environments/examples/minigrid/agent_demos/minigrid_react_agent.py +0 -562
  786. synth_ai/environments/examples/minigrid/agent_demos/minigrid_trace_evaluation.py +0 -221
  787. synth_ai/environments/examples/minigrid/engine.py +0 -589
  788. synth_ai/environments/examples/minigrid/environment.py +0 -274
  789. synth_ai/environments/examples/minigrid/environment_mapping.py +0 -242
  790. synth_ai/environments/examples/minigrid/puzzle_loader.py +0 -417
  791. synth_ai/environments/examples/minigrid/taskset.py +0 -583
  792. synth_ai/environments/examples/nethack/__init__.py +0 -7
  793. synth_ai/environments/examples/nethack/achievements.py +0 -337
  794. synth_ai/environments/examples/nethack/agent_demos/nethack_evaluation_framework.py +0 -981
  795. synth_ai/environments/examples/nethack/agent_demos/nethack_quick_evaluation.py +0 -74
  796. synth_ai/environments/examples/nethack/agent_demos/nethack_react_agent.py +0 -831
  797. synth_ai/environments/examples/nethack/engine.py +0 -739
  798. synth_ai/environments/examples/nethack/environment.py +0 -256
  799. synth_ai/environments/examples/nethack/helpers/__init__.py +0 -41
  800. synth_ai/environments/examples/nethack/helpers/action_mapping.py +0 -301
  801. synth_ai/environments/examples/nethack/helpers/nle_wrapper.py +0 -402
  802. synth_ai/environments/examples/nethack/helpers/observation_utils.py +0 -433
  803. synth_ai/environments/examples/nethack/helpers/recording_wrapper.py +0 -200
  804. synth_ai/environments/examples/nethack/helpers/trajectory_recorder.py +0 -269
  805. synth_ai/environments/examples/nethack/helpers/visualization/replay_viewer.py +0 -308
  806. synth_ai/environments/examples/nethack/helpers/visualization/visualizer.py +0 -431
  807. synth_ai/environments/examples/nethack/taskset.py +0 -323
  808. synth_ai/environments/examples/red/__init__.py +0 -7
  809. synth_ai/environments/examples/red/agent_demos/__init__.py +0 -1
  810. synth_ai/environments/examples/red/config_logging.py +0 -110
  811. synth_ai/environments/examples/red/engine.py +0 -721
  812. synth_ai/environments/examples/red/engine_helpers/__init__.py +0 -1
  813. synth_ai/environments/examples/red/engine_helpers/memory_map.py +0 -35
  814. synth_ai/environments/examples/red/engine_helpers/reward_components.py +0 -276
  815. synth_ai/environments/examples/red/engine_helpers/reward_library/__init__.py +0 -142
  816. synth_ai/environments/examples/red/engine_helpers/reward_library/adaptive_rewards.py +0 -57
  817. synth_ai/environments/examples/red/engine_helpers/reward_library/battle_rewards.py +0 -284
  818. synth_ai/environments/examples/red/engine_helpers/reward_library/composite_rewards.py +0 -150
  819. synth_ai/environments/examples/red/engine_helpers/reward_library/economy_rewards.py +0 -138
  820. synth_ai/environments/examples/red/engine_helpers/reward_library/efficiency_rewards.py +0 -57
  821. synth_ai/environments/examples/red/engine_helpers/reward_library/exploration_rewards.py +0 -331
  822. synth_ai/environments/examples/red/engine_helpers/reward_library/novelty_rewards.py +0 -121
  823. synth_ai/environments/examples/red/engine_helpers/reward_library/pallet_town_progression.py +0 -477
  824. synth_ai/environments/examples/red/engine_helpers/reward_library/pallet_town_rewards.py +0 -559
  825. synth_ai/environments/examples/red/engine_helpers/reward_library/pokemon_rewards.py +0 -313
  826. synth_ai/environments/examples/red/engine_helpers/reward_library/social_rewards.py +0 -148
  827. synth_ai/environments/examples/red/engine_helpers/reward_library/story_rewards.py +0 -247
  828. synth_ai/environments/examples/red/engine_helpers/screen_analysis.py +0 -368
  829. synth_ai/environments/examples/red/engine_helpers/state_extraction.py +0 -172
  830. synth_ai/environments/examples/red/environment.py +0 -298
  831. synth_ai/environments/examples/red/taskset.py +0 -79
  832. synth_ai/environments/examples/red/units/__init__.py +0 -1
  833. synth_ai/environments/examples/sokoban/__init__.py +0 -1
  834. synth_ai/environments/examples/sokoban/agent_demos/sokoban_full_eval.py +0 -899
  835. synth_ai/environments/examples/sokoban/engine.py +0 -678
  836. synth_ai/environments/examples/sokoban/engine_helpers/__init__.py +0 -1
  837. synth_ai/environments/examples/sokoban/engine_helpers/room_utils.py +0 -657
  838. synth_ai/environments/examples/sokoban/engine_helpers/vendored/__init__.py +0 -18
  839. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/__init__.py +0 -3
  840. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/boxoban_env.py +0 -131
  841. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/render_utils.py +0 -370
  842. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/room_utils.py +0 -332
  843. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env.py +0 -306
  844. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_fixed_targets.py +0 -67
  845. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_pull.py +0 -115
  846. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_two_player.py +0 -123
  847. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_variations.py +0 -394
  848. synth_ai/environments/examples/sokoban/environment.py +0 -229
  849. synth_ai/environments/examples/sokoban/generate_verified_puzzles.py +0 -440
  850. synth_ai/environments/examples/sokoban/puzzle_loader.py +0 -312
  851. synth_ai/environments/examples/sokoban/taskset.py +0 -544
  852. synth_ai/environments/examples/tictactoe/__init__.py +0 -1
  853. synth_ai/environments/examples/tictactoe/engine.py +0 -368
  854. synth_ai/environments/examples/tictactoe/environment.py +0 -240
  855. synth_ai/environments/examples/tictactoe/taskset.py +0 -215
  856. synth_ai/environments/examples/verilog/__init__.py +0 -10
  857. synth_ai/environments/examples/verilog/engine.py +0 -421
  858. synth_ai/environments/examples/verilog/environment.py +0 -350
  859. synth_ai/environments/examples/verilog/taskset.py +0 -420
  860. synth_ai/environments/examples/wordle/__init__.py +0 -29
  861. synth_ai/environments/examples/wordle/engine.py +0 -398
  862. synth_ai/environments/examples/wordle/environment.py +0 -159
  863. synth_ai/environments/examples/wordle/helpers/generate_instances_wordfreq.py +0 -75
  864. synth_ai/environments/examples/wordle/taskset.py +0 -230
  865. synth_ai/environments/reproducibility/core.py +0 -42
  866. synth_ai/environments/reproducibility/helpers.py +0 -0
  867. synth_ai/environments/reproducibility/tree.py +0 -363
  868. synth_ai/environments/service/app.py +0 -97
  869. synth_ai/environments/service/core_routes.py +0 -1021
  870. synth_ai/environments/service/external_registry.py +0 -56
  871. synth_ai/environments/service/registry.py +0 -9
  872. synth_ai/environments/stateful/__init__.py +0 -1
  873. synth_ai/environments/stateful/core.py +0 -163
  874. synth_ai/environments/stateful/engine.py +0 -21
  875. synth_ai/environments/stateful/state.py +0 -7
  876. synth_ai/environments/tasks/api.py +0 -19
  877. synth_ai/environments/tasks/core.py +0 -81
  878. synth_ai/environments/tasks/filters.py +0 -40
  879. synth_ai/environments/tasks/utils.py +0 -90
  880. synth_ai/environments/v0_observability/history.py +0 -3
  881. synth_ai/environments/v0_observability/log.py +0 -2
  882. synth_ai/evals/__init__.py +0 -15
  883. synth_ai/evals/base.py +0 -13
  884. synth_ai/evals/client.py +0 -82
  885. synth_ai/evals/types.py +0 -42
  886. synth_ai/handshake.py +0 -109
  887. synth_ai/http.py +0 -26
  888. synth_ai/http_client.py +0 -136
  889. synth_ai/inference/__init__.py +0 -5
  890. synth_ai/inference/client.py +0 -34
  891. synth_ai/jobs/client.py +0 -295
  892. synth_ai/judge_schemas.py +0 -127
  893. synth_ai/learning/__init__.py +0 -59
  894. synth_ai/learning/client.py +0 -241
  895. synth_ai/learning/ft_client.py +0 -7
  896. synth_ai/learning/health.py +0 -49
  897. synth_ai/learning/jobs.py +0 -201
  898. synth_ai/learning/rl/__init__.py +0 -39
  899. synth_ai/learning/rl/client.py +0 -267
  900. synth_ai/learning/rl/contracts.py +0 -27
  901. synth_ai/learning/rl/env_keys.py +0 -166
  902. synth_ai/learning/rl/secrets.py +0 -13
  903. synth_ai/learning/sft/client.py +0 -68
  904. synth_ai/learning/sft/config.py +0 -270
  905. synth_ai/learning/sft/data.py +0 -295
  906. synth_ai/learning/validators.py +0 -49
  907. synth_ai/lm/__init__.py +0 -25
  908. synth_ai/task/__init__.py +0 -121
  909. synth_ai/task/apps/__init__.py +0 -129
  910. synth_ai/task/client.py +0 -167
  911. synth_ai/task/config.py +0 -257
  912. synth_ai/task/contracts.py +0 -236
  913. synth_ai/task/datasets.py +0 -108
  914. synth_ai/task/proxy.py +0 -251
  915. synth_ai/task/rubrics/__init__.py +0 -56
  916. synth_ai/task/rubrics/loaders.py +0 -152
  917. synth_ai/task/rubrics/strict.py +0 -149
  918. synth_ai/task/server.py +0 -432
  919. synth_ai/task/trace_correlation_helpers.py +0 -315
  920. synth_ai/task/tracing_utils.py +0 -84
  921. synth_ai/task/validators.py +0 -418
  922. synth_ai/tracing_v3/__init__.py +0 -97
  923. synth_ai/tracing_v3/abstractions.py +0 -302
  924. synth_ai/tracing_v3/config.py +0 -84
  925. synth_ai/tracing_v3/db_config.py +0 -194
  926. synth_ai/tracing_v3/decorators.py +0 -398
  927. synth_ai/tracing_v3/llm_call_record_helpers.py +0 -391
  928. synth_ai/tracing_v3/migration_helper.py +0 -120
  929. synth_ai/tracing_v3/session_tracer.py +0 -540
  930. synth_ai/tracing_v3/storage/base.py +0 -210
  931. synth_ai/tracing_v3/storage/config.py +0 -75
  932. synth_ai/tracing_v3/storage/factory.py +0 -39
  933. synth_ai/tracing_v3/trace_utils.py +0 -317
  934. synth_ai/tracing_v3/turso/daemon.py +0 -151
  935. synth_ai/tracing_v3/turso/models.py +0 -469
  936. synth_ai/tracing_v3/turso/native_manager.py +0 -1209
  937. synth_ai/tracing_v3/utils.py +0 -108
  938. synth_ai/tui/__init__.py +0 -5
  939. synth_ai/tui/__main__.py +0 -13
  940. synth_ai/tui/cli/__init__.py +0 -1
  941. synth_ai/tui/cli/query_experiments.py +0 -164
  942. synth_ai/tui/cli/query_experiments_v3.py +0 -164
  943. synth_ai/tui/dashboard.py +0 -906
  944. synth_ai/v0/api/__init__.py +0 -8
  945. synth_ai/v0/api/models/__init__.py +0 -8
  946. synth_ai/v0/api/models/supported.py +0 -8
  947. synth_ai/v0/config/__init__.py +0 -15
  948. synth_ai/v0/config/base_url.py +0 -12
  949. synth_ai/v0/lm/__init__.py +0 -51
  950. synth_ai/v0/lm/caching/__init__.py +0 -0
  951. synth_ai/v0/lm/caching/constants.py +0 -6
  952. synth_ai/v0/lm/caching/dbs.py +0 -0
  953. synth_ai/v0/lm/caching/ephemeral.py +0 -100
  954. synth_ai/v0/lm/caching/handler.py +0 -137
  955. synth_ai/v0/lm/caching/initialize.py +0 -11
  956. synth_ai/v0/lm/caching/persistent.py +0 -114
  957. synth_ai/v0/lm/config.py +0 -115
  958. synth_ai/v0/lm/constants.py +0 -32
  959. synth_ai/v0/lm/core/__init__.py +0 -8
  960. synth_ai/v0/lm/core/all.py +0 -73
  961. synth_ai/v0/lm/core/exceptions.py +0 -5
  962. synth_ai/v0/lm/core/main.py +0 -331
  963. synth_ai/v0/lm/core/main_v3.py +0 -594
  964. synth_ai/v0/lm/core/synth_models.py +0 -35
  965. synth_ai/v0/lm/core/vendor_clients.py +0 -190
  966. synth_ai/v0/lm/cost/__init__.py +0 -0
  967. synth_ai/v0/lm/cost/monitor.py +0 -1
  968. synth_ai/v0/lm/cost/statefulness.py +0 -1
  969. synth_ai/v0/lm/injection.py +0 -80
  970. synth_ai/v0/lm/overrides.py +0 -206
  971. synth_ai/v0/lm/provider_support/__init__.py +0 -8
  972. synth_ai/v0/lm/provider_support/anthropic.py +0 -972
  973. synth_ai/v0/lm/provider_support/openai.py +0 -1139
  974. synth_ai/v0/lm/provider_support/suppress_logging.py +0 -31
  975. synth_ai/v0/lm/structured_outputs/__init__.py +0 -0
  976. synth_ai/v0/lm/structured_outputs/handler.py +0 -440
  977. synth_ai/v0/lm/structured_outputs/inject.py +0 -297
  978. synth_ai/v0/lm/structured_outputs/rehabilitate.py +0 -185
  979. synth_ai/v0/lm/tools/__init__.py +0 -3
  980. synth_ai/v0/lm/tools/base.py +0 -172
  981. synth_ai/v0/lm/unified_interface.py +0 -202
  982. synth_ai/v0/lm/vendors/__init__.py +0 -0
  983. synth_ai/v0/lm/vendors/base.py +0 -81
  984. synth_ai/v0/lm/vendors/core/__init__.py +0 -0
  985. synth_ai/v0/lm/vendors/core/anthropic_api.py +0 -387
  986. synth_ai/v0/lm/vendors/core/gemini_api.py +0 -292
  987. synth_ai/v0/lm/vendors/core/mistral_api.py +0 -322
  988. synth_ai/v0/lm/vendors/core/openai_api.py +0 -227
  989. synth_ai/v0/lm/vendors/core/synth_dev_api.py +0 -0
  990. synth_ai/v0/lm/vendors/local/__init__.py +0 -0
  991. synth_ai/v0/lm/vendors/local/ollama.py +0 -0
  992. synth_ai/v0/lm/vendors/openai_standard.py +0 -782
  993. synth_ai/v0/lm/vendors/openai_standard_responses.py +0 -259
  994. synth_ai/v0/lm/vendors/retries.py +0 -22
  995. synth_ai/v0/lm/vendors/supported/__init__.py +0 -0
  996. synth_ai/v0/lm/vendors/supported/custom_endpoint.py +0 -415
  997. synth_ai/v0/lm/vendors/supported/deepseek.py +0 -69
  998. synth_ai/v0/lm/vendors/supported/grok.py +0 -75
  999. synth_ai/v0/lm/vendors/supported/groq.py +0 -16
  1000. synth_ai/v0/lm/vendors/supported/ollama.py +0 -15
  1001. synth_ai/v0/lm/vendors/supported/openrouter.py +0 -74
  1002. synth_ai/v0/lm/vendors/supported/together.py +0 -11
  1003. synth_ai/v0/lm/vendors/synth_client.py +0 -835
  1004. synth_ai/v0/lm/warmup.py +0 -186
  1005. synth_ai/v0/tracing/__init__.py +0 -0
  1006. synth_ai/v0/tracing/abstractions.py +0 -224
  1007. synth_ai/v0/tracing/base_client.py +0 -91
  1008. synth_ai/v0/tracing/client_manager.py +0 -131
  1009. synth_ai/v0/tracing/config.py +0 -142
  1010. synth_ai/v0/tracing/context.py +0 -146
  1011. synth_ai/v0/tracing/decorators.py +0 -682
  1012. synth_ai/v0/tracing/events/__init__.py +0 -0
  1013. synth_ai/v0/tracing/events/manage.py +0 -147
  1014. synth_ai/v0/tracing/events/scope.py +0 -86
  1015. synth_ai/v0/tracing/events/store.py +0 -228
  1016. synth_ai/v0/tracing/immediate_client.py +0 -151
  1017. synth_ai/v0/tracing/local.py +0 -18
  1018. synth_ai/v0/tracing/log_client_base.py +0 -73
  1019. synth_ai/v0/tracing/retry_queue.py +0 -186
  1020. synth_ai/v0/tracing/trackers.py +0 -515
  1021. synth_ai/v0/tracing/upload.py +0 -409
  1022. synth_ai/v0/tracing/utils.py +0 -9
  1023. synth_ai/v0/tracing_v1/__init__.py +0 -16
  1024. synth_ai/v0/tracing_v1/abstractions.py +0 -224
  1025. synth_ai/v0/tracing_v1/base_client.py +0 -91
  1026. synth_ai/v0/tracing_v1/client_manager.py +0 -131
  1027. synth_ai/v0/tracing_v1/config.py +0 -142
  1028. synth_ai/v0/tracing_v1/context.py +0 -146
  1029. synth_ai/v0/tracing_v1/decorators.py +0 -703
  1030. synth_ai/v0/tracing_v1/events/__init__.py +0 -0
  1031. synth_ai/v0/tracing_v1/events/manage.py +0 -147
  1032. synth_ai/v0/tracing_v1/events/scope.py +0 -86
  1033. synth_ai/v0/tracing_v1/events/store.py +0 -228
  1034. synth_ai/v0/tracing_v1/immediate_client.py +0 -151
  1035. synth_ai/v0/tracing_v1/local.py +0 -18
  1036. synth_ai/v0/tracing_v1/log_client_base.py +0 -73
  1037. synth_ai/v0/tracing_v1/retry_queue.py +0 -186
  1038. synth_ai/v0/tracing_v1/trackers.py +0 -515
  1039. synth_ai/v0/tracing_v1/upload.py +0 -527
  1040. synth_ai/v0/tracing_v1/utils.py +0 -9
  1041. synth_ai/v0/tracing_v3/__init__.py +0 -10
  1042. synth_ai/v0/tracing_v3/abstractions.py +0 -3
  1043. synth_ai/v0/tracing_v3/decorators.py +0 -3
  1044. synth_ai/v0/tracing_v3/llm_call_record_helpers.py +0 -3
  1045. synth_ai/v0/tracing_v3/session_tracer.py +0 -3
  1046. synth_ai-0.2.14.dist-info/METADATA +0 -139
  1047. synth_ai-0.2.14.dist-info/RECORD +0 -762
  1048. synth_ai-0.2.14.dist-info/top_level.txt +0 -2
  1049. /synth_ai/{demos/demo_task_apps → cli/demo_apps}/crafter/__init__.py +0 -0
  1050. /synth_ai/{demos → cli/demo_apps}/demo_task_apps/__init__.py +0 -0
  1051. /synth_ai/{demos → cli/demo_apps}/demo_task_apps/crafter/configs/crafter_fft_4b.toml +0 -0
  1052. /synth_ai/{demos → cli/demo_apps}/demo_task_apps/crafter/configs/rl_from_base_qwen4b.toml +0 -0
  1053. /synth_ai/{demos → cli/demo_apps}/demo_task_apps/math/__init__.py +0 -0
  1054. /synth_ai/{demos → cli/demo_apps}/demo_task_apps/math/_common.py +0 -0
  1055. /synth_ai/{demos → cli/demo_apps}/demo_task_apps/math/app.py +0 -0
  1056. /synth_ai/{demos → cli/demo_apps}/demo_task_apps/math/deploy_modal.py +0 -0
  1057. {examples/task_apps → synth_ai/core/apps}/__init__.py +0 -0
  1058. /synth_ai/{tracing_v3 → core/tracing_v3}/examples/basic_usage.py +0 -0
  1059. /synth_ai/{tracing_v3 → core/tracing_v3}/hooks.py +0 -0
  1060. /synth_ai/{tracing_v3 → core/tracing_v3}/lm_call_record_abstractions.py +0 -0
  1061. /synth_ai/{tracing_v3 → core/tracing_v3}/replica_sync.py +0 -0
  1062. /synth_ai/{tracing_v3 → core/tracing_v3}/serialization.py +0 -0
  1063. /synth_ai/{tracing_v3 → core/tracing_v3}/storage/__init__.py +0 -0
  1064. /synth_ai/{tracing_v3 → core/tracing_v3}/storage/exceptions.py +0 -0
  1065. /synth_ai/{tracing_v3 → core/tracing_v3}/storage/types.py +0 -0
  1066. /synth_ai/{tracing_v3 → core/tracing_v3}/storage/utils.py +0 -0
  1067. /synth_ai/{tracing_v3 → core/tracing_v3}/turso/__init__.py +0 -0
  1068. /synth_ai/{learning → sdk/learning}/algorithms.py +0 -0
  1069. /synth_ai/{learning → sdk/learning}/config.py +0 -0
  1070. /synth_ai/{learning → sdk/learning}/constants.py +0 -0
  1071. /synth_ai/{learning → sdk/learning}/core.py +0 -0
  1072. /synth_ai/{learning → sdk/learning}/gateway.py +0 -0
  1073. /synth_ai/{learning → sdk/learning}/rl/config.py +0 -0
  1074. /synth_ai/{learning → sdk/learning}/rl_client.py +0 -0
  1075. /synth_ai/{learning → sdk/learning}/sft/__init__.py +0 -0
  1076. /synth_ai/{learning → sdk/learning}/sse.py +0 -0
  1077. /synth_ai/{task → sdk/task}/auth.py +0 -0
  1078. /synth_ai/{task → sdk/task}/errors.py +0 -0
  1079. /synth_ai/{task → sdk/task}/health.py +0 -0
  1080. /synth_ai/{task → sdk/task}/json.py +0 -0
  1081. /synth_ai/{task → sdk/task}/rubrics/models.py +0 -0
  1082. /synth_ai/{task → sdk/task}/rubrics/scoring.py +0 -0
  1083. /synth_ai/{task → sdk/task}/vendors.py +0 -0
  1084. {synth_ai-0.2.14.dist-info → synth_ai-0.4.4.dist-info}/WHEEL +0 -0
  1085. {synth_ai-0.2.14.dist-info → synth_ai-0.4.4.dist-info}/entry_points.txt +0 -0
  1086. {synth_ai-0.2.14.dist-info → synth_ai-0.4.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1886 @@
1
+ """Cloudflare CLI/bootstrap helpers and tunnel deployment utilities."""
2
+
3
+ import asyncio
4
+ import logging
5
+ import os
6
+ import platform
7
+ import re
8
+ import shutil
9
+ import signal
10
+ import socket
11
+ import subprocess
12
+ import sys
13
+ import tarfile
14
+ import tempfile
15
+ import time
16
+ from dataclasses import dataclass
17
+ from pathlib import Path
18
+ from typing import Any, Iterable, Optional, Tuple
19
+ from urllib.parse import urlparse
20
+
21
+ import click
22
+ import httpx
23
+ import requests
24
+ import uvicorn
25
+ from starlette.types import ASGIApp
26
+
27
+ from synth_ai.core.apps.common import get_asgi_app, load_module
28
+ from synth_ai.core.cfgs import CFDeployCfg
29
+ from synth_ai.core.paths import REPO_ROOT, configure_import_paths
30
+ from synth_ai.core.telemetry import log_error, log_event, log_info
31
+ from synth_ai.core.urls import BACKEND_URL_BASE
32
+
33
+
34
+ def __resolve_env_var(key: str) -> str:
35
+ """Lazy import to avoid circular dependency."""
36
+ from synth_ai.cli.lib.env import resolve_env_var
37
+ return resolve_env_var(key)
38
+
39
+
40
+ def __write_env_var_to_dotenv(key: str, value: str, **kwargs) -> None:
41
+ """Lazy import to avoid circular dependency."""
42
+ from synth_ai.cli.lib.env import write_env_var_to_dotenv
43
+ write_env_var_to_dotenv(key, value, **kwargs)
44
+
45
+ logger = logging.getLogger(__name__)
46
+
47
+ # Constants
48
+ CLOUDFLARED_BIN_NAME = "cloudflared"
49
+ CLOUDFLARED_RELEASES = "https://updatecloudflared.com/launcher"
50
+ CLOUDFLARE_DOCS_URL = "https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation"
51
+
52
+ # Regex for parsing quick tunnel URLs
53
+ # Match partial URLs too (in case they're split across lines)
54
+ _URL_RE = re.compile(r"https://[a-z0-9-]+\.trycloudflare\.com", re.I)
55
+ _URL_PARTIAL_RE = re.compile(r"https://[a-z0-9-]+\.trycloudf", re.I) # Partial match for truncated lines (ends with trycloudf)
56
+ _URL_PARTIAL_RE2 = re.compile(r"https://[a-z0-9-]+\.tryclo", re.I) # Partial match for truncated lines (ends with tryclo)
57
+
58
+ # Global state - store tunnel process handles for cleanup
59
+ _TUNNEL_PROCESSES: dict[int, subprocess.Popen] = {}
60
+
61
+
62
+ @dataclass(slots=True)
63
+ class ManagedTunnelRecord:
64
+ """Managed tunnel metadata returned by backend."""
65
+
66
+ id: str
67
+ hostname: str
68
+ org_id: str
69
+ org_name: Optional[str]
70
+ local_host: str
71
+ local_port: int
72
+ metadata: dict[str, Any]
73
+ raw: dict[str, Any]
74
+
75
+ @property
76
+ def url(self) -> str:
77
+ if self.hostname.startswith(("http://", "https://")):
78
+ return self.hostname
79
+ return f"https://{self.hostname}"
80
+
81
+ @property
82
+ def subdomain(self) -> str:
83
+ return self.hostname.split(".", 1)[0]
84
+
85
+ def credential(self, key: str) -> Optional[str]:
86
+ return _extract_credential(self.raw, key)
87
+
88
+
89
+ # ---------------------------------------------------------------------------
90
+ # Managed tunnel discovery helpers
91
+ # ---------------------------------------------------------------------------
92
+
93
+
94
+ async def fetch_managed_tunnels(synth_api_key: str) -> list[ManagedTunnelRecord]:
95
+ """
96
+ Fetch managed tunnels tied to the provided Synth API key.
97
+
98
+ Raises:
99
+ RuntimeError: If backend returns an error or unexpected payload.
100
+ """
101
+ url = f"{BACKEND_URL_BASE}/api/v1/tunnels/"
102
+ headers = {"Authorization": f"Bearer {synth_api_key}"}
103
+ try:
104
+ async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
105
+ response = await client.get(url, headers=headers)
106
+ response.raise_for_status()
107
+ payload = response.json()
108
+ except httpx.HTTPStatusError as exc:
109
+ raise RuntimeError(
110
+ f"Failed to list managed tunnels (status {exc.response.status_code}): {exc.response.text}"
111
+ ) from exc
112
+ except httpx.RequestError as exc:
113
+ raise RuntimeError(f"Failed to reach Synth backend at {url}: {exc}") from exc
114
+
115
+ if not isinstance(payload, list):
116
+ raise RuntimeError("Unexpected tunnel API response: expected a list of tunnels.")
117
+
118
+ records: list[ManagedTunnelRecord] = []
119
+ for entry in payload:
120
+ if not isinstance(entry, dict):
121
+ continue
122
+ hostname = entry.get("hostname")
123
+ org_id = entry.get("org_id")
124
+ tunnel_id = entry.get("id")
125
+ if not hostname or not org_id or not tunnel_id:
126
+ continue
127
+ metadata = entry.get("metadata")
128
+ if not isinstance(metadata, dict):
129
+ metadata = {}
130
+ records.append(
131
+ ManagedTunnelRecord(
132
+ id=str(tunnel_id),
133
+ hostname=str(hostname),
134
+ org_id=str(org_id),
135
+ org_name=entry.get("org_name"),
136
+ local_host=str(entry.get("local_host") or "127.0.0.1"),
137
+ local_port=int(entry.get("local_port") or 8000),
138
+ metadata=metadata,
139
+ raw=entry,
140
+ )
141
+ )
142
+ return records
143
+
144
+
145
+ def _select_existing_tunnel(
146
+ tunnels: list[ManagedTunnelRecord],
147
+ desired_subdomain: Optional[str],
148
+ ) -> Optional[ManagedTunnelRecord]:
149
+ if not tunnels:
150
+ return None
151
+
152
+ if desired_subdomain:
153
+ target = _normalize_subdomain(desired_subdomain)
154
+ for tunnel in tunnels:
155
+ if _normalize_subdomain(tunnel.subdomain) == target or _normalize_subdomain(
156
+ tunnel.hostname
157
+ ) == target:
158
+ print(
159
+ f"ℹ️ Using managed tunnel {tunnel.url} "
160
+ f"(matched subdomain '{tunnel.subdomain}')"
161
+ )
162
+ return tunnel
163
+ _print_tunnel_choices(tunnels, header="Available managed tunnels:")
164
+ raise RuntimeError(
165
+ f"No managed tunnel matched subdomain '{desired_subdomain}'. "
166
+ "Re-run with a valid --tunnel-subdomain."
167
+ )
168
+
169
+ if len(tunnels) == 1:
170
+ tunnel = tunnels[0]
171
+ print(
172
+ f"ℹ️ Reusing existing managed tunnel for "
173
+ f"{tunnel.org_name or tunnel.org_id}: {tunnel.url}"
174
+ )
175
+ return tunnel
176
+
177
+ _print_tunnel_choices(
178
+ tunnels,
179
+ header=(
180
+ "Multiple managed tunnels found. Please re-run with "
181
+ "--tunnel-subdomain <subdomain> to choose one."
182
+ ),
183
+ )
184
+ raise RuntimeError("Multiple managed tunnels available; selection required.")
185
+
186
+
187
+ def _print_tunnel_choices(
188
+ tunnels: Iterable[ManagedTunnelRecord],
189
+ header: Optional[str] = None,
190
+ ) -> None:
191
+ if header:
192
+ print(header)
193
+ for idx, tunnel in enumerate(tunnels, 1):
194
+ label = tunnel.org_name or tunnel.org_id
195
+ print(f" {idx}. {label}: {tunnel.url} (subdomain '{tunnel.subdomain}')")
196
+
197
+
198
+ def _normalize_subdomain(value: str) -> str:
199
+ value = value.strip().lower()
200
+ if value.startswith("https://"):
201
+ value = value[len("https://") :]
202
+ elif value.startswith("http://"):
203
+ value = value[len("http://") :]
204
+ return value.split(".", 1)[0]
205
+
206
+
207
+ def _extract_credential(payload: dict[str, Any], key: str) -> Optional[str]:
208
+ """Extract secret from various nested metadata structures."""
209
+
210
+ def _dig(obj: Any, path: tuple[str, ...]) -> Optional[Any]:
211
+ current = obj
212
+ for part in path:
213
+ if isinstance(current, dict):
214
+ current = current.get(part)
215
+ else:
216
+ return None
217
+ return current
218
+
219
+ candidate_paths: tuple[tuple[str, ...], ...] = (
220
+ (key,),
221
+ ("metadata", key),
222
+ ("metadata", "secrets", key),
223
+ ("metadata", "credentials", key),
224
+ ("metadata", "cloudflare", key),
225
+ ("metadata", "cloudflare", "secrets", key),
226
+ )
227
+
228
+ for path in candidate_paths:
229
+ value = _dig(payload, path)
230
+ if isinstance(value, str) and value:
231
+ return value
232
+ return None
233
+
234
+
235
+ # ---------------------------------------------------------------------------
236
+ # Cloudflared binary management
237
+ # ---------------------------------------------------------------------------
238
+
239
+
240
+ def get_cloudflared_path(prefer_system: bool = True) -> Optional[Path]:
241
+ """Locate the cloudflared binary (managed bin dir, PATH, or common dirs)."""
242
+ bin_dir = Path.home() / ".synth-ai" / "bin"
243
+ candidate = bin_dir / CLOUDFLARED_BIN_NAME
244
+ if candidate.exists() and os.access(candidate, os.X_OK):
245
+ return candidate
246
+
247
+ if prefer_system:
248
+ resolved = shutil.which(CLOUDFLARED_BIN_NAME)
249
+ if resolved:
250
+ return Path(resolved)
251
+
252
+ common = [
253
+ Path("/usr/local/bin/cloudflared"),
254
+ Path("/opt/homebrew/bin/cloudflared"),
255
+ Path.home() / "bin" / "cloudflared",
256
+ ]
257
+ for path in common:
258
+ if path.exists() and os.access(path, os.X_OK):
259
+ return path
260
+ return None
261
+
262
+
263
+ def ensure_cloudflared_installed(force: bool = False) -> Path:
264
+ """Ensure cloudflared is installed in synth-ai's managed bin directory."""
265
+ existing = get_cloudflared_path(prefer_system=not force)
266
+ if existing and not force:
267
+ return existing
268
+
269
+ target_dir = Path.home() / ".synth-ai" / "bin"
270
+ target_dir.mkdir(parents=True, exist_ok=True)
271
+
272
+ url = _resolve_cloudflared_download_url()
273
+ tmp_file = _download_file(url)
274
+
275
+ if tmp_file.suffixes[-2:] == [".tar", ".gz"]:
276
+ _extract_tarball(tmp_file, target_dir)
277
+ elif tmp_file.suffix == ".gz":
278
+ _extract_gzip(tmp_file, target_dir / CLOUDFLARED_BIN_NAME)
279
+ else:
280
+ shutil.move(str(tmp_file), str(target_dir / CLOUDFLARED_BIN_NAME))
281
+
282
+ bin_path = target_dir / CLOUDFLARED_BIN_NAME
283
+ bin_path.chmod(0o755)
284
+ log_event("info", "cloudflared installed", ctx={"path": str(bin_path)})
285
+ return bin_path
286
+
287
+
288
+ def require_cloudflared() -> Path:
289
+ """Return cloudflared binary or raise ClickException with guidance."""
290
+ path = get_cloudflared_path()
291
+ if path:
292
+ return path
293
+
294
+ extra = ""
295
+ if platform.system() == "Darwin":
296
+ extra = "Try `brew install cloudflare/cloudflare/cloudflared`."
297
+ elif platform.system() == "Linux":
298
+ extra = "See Cloudflare docs for Linux packages."
299
+ log_error("cloudflared not found", ctx={"hint": extra})
300
+ raise click.ClickException(
301
+ f"Cloudflared CLI missing. Install via Homebrew or follow {CLOUDFLARE_DOCS_URL}."
302
+ )
303
+
304
+
305
+ def run_cloudflared_cmd(args: list[str], *, env: Optional[dict[str, str]] = None) -> subprocess.Popen:
306
+ """Spawn cloudflared subprocess (mirrors synth_ai.core.integrations.modal.run_modal_cmd)."""
307
+ bin_path = require_cloudflared()
308
+ cmd = [str(bin_path), *args]
309
+ log_event("info", "starting cloudflared", ctx={"cmd": cmd})
310
+ try:
311
+ return subprocess.Popen(
312
+ cmd,
313
+ stdout=subprocess.PIPE,
314
+ stderr=subprocess.STDOUT,
315
+ text=True,
316
+ bufsize=1,
317
+ env=env or os.environ.copy(),
318
+ )
319
+ except FileNotFoundError as exc:
320
+ raise click.ClickException(f"cloudflared binary missing: {exc}") from exc
321
+ except Exception as exc:
322
+ raise click.ClickException(f"Failed to start cloudflared: {exc}") from exc
323
+
324
+
325
+ # ---------------------------------------------------------------------------
326
+ # Internal helpers
327
+ # ---------------------------------------------------------------------------
328
+
329
+ def _resolve_cloudflared_download_url() -> str:
330
+ system = platform.system().lower()
331
+ arch = platform.machine().lower()
332
+ mapping = {"darwin": "macos", "linux": "linux", "windows": "windows"}
333
+ platform_key = mapping.get(system)
334
+ if not platform_key:
335
+ raise RuntimeError(f"Unsupported platform: {system}")
336
+
337
+ arch_key = "amd64"
338
+ if arch in ("arm64", "aarch64"):
339
+ arch_key = "arm64"
340
+
341
+ resp = requests.get(f"{CLOUDFLARED_RELEASES}/v1/{platform_key}/{arch_key}/versions/stable", timeout=30.0)
342
+ resp.raise_for_status()
343
+ data = resp.json()
344
+ url = data.get("url")
345
+ if not url:
346
+ raise RuntimeError("Cloudflared release metadata missing URL")
347
+ return url
348
+
349
+
350
+ def _download_file(url: str) -> Path:
351
+ resp = requests.get(url, timeout=60.0, stream=True)
352
+ resp.raise_for_status()
353
+ suffix = Path(url.split("?")[0]).suffix or ".tmp"
354
+ fd, tmp_path = tempfile.mkstemp(suffix=suffix)
355
+ with os.fdopen(fd, "wb") as fh:
356
+ for chunk in resp.iter_content(chunk_size=8192):
357
+ fh.write(chunk)
358
+ return Path(tmp_path)
359
+
360
+
361
+ def _extract_tarball(archive_path: Path, target_dir: Path) -> None:
362
+ with tarfile.open(archive_path, "r:gz") as tar:
363
+ tar.extractall(target_dir)
364
+ archive_path.unlink(missing_ok=True)
365
+
366
+
367
+ def _extract_gzip(gz_path: Path, target: Path) -> None:
368
+ import gzip
369
+
370
+ # gzip.open ensures the bytes are decompressed while copying to target
371
+ with gzip.open(gz_path, "rb") as gz_fh, open(target, "wb") as out_fh:
372
+ shutil.copyfileobj(gz_fh, out_fh)
373
+ gz_path.unlink(missing_ok=True)
374
+
375
+
376
+ # ---------------------------------------------------------------------------
377
+ # Tunnel process management
378
+ # ---------------------------------------------------------------------------
379
+
380
+
381
+ def open_quick_tunnel(port: int, wait_s: float = 10.0) -> Tuple[str, subprocess.Popen]:
382
+ """
383
+ Open a quick (ephemeral) Cloudflare tunnel.
384
+
385
+ Args:
386
+ port: Local port to tunnel to
387
+ wait_s: Maximum time to wait for URL in seconds
388
+
389
+ Returns:
390
+ Tuple of (public_url, process_handle)
391
+
392
+ Raises:
393
+ RuntimeError: If tunnel fails to start or URL cannot be parsed
394
+ """
395
+ bin_path = require_cloudflared()
396
+
397
+ # Verify cloudflared can run before attempting tunnel
398
+ try:
399
+ test_proc = subprocess.run(
400
+ [str(bin_path), "--version"],
401
+ capture_output=True,
402
+ text=True,
403
+ timeout=5.0,
404
+ )
405
+ if test_proc.returncode != 0:
406
+ raise RuntimeError(
407
+ f"cloudflared binary exists but fails to run (exit code {test_proc.returncode}). "
408
+ f"STDOUT: {test_proc.stdout[:500] if test_proc.stdout else 'none'}. "
409
+ f"STDERR: {test_proc.stderr[:500] if test_proc.stderr else 'none'}. "
410
+ f"Try reinstalling: cloudflared update or brew reinstall cloudflared"
411
+ )
412
+ except subprocess.TimeoutExpired as e:
413
+ raise RuntimeError(
414
+ "cloudflared binary hangs when running --version. "
415
+ "This suggests the binary is corrupted or incompatible with your system. "
416
+ "Try reinstalling: cloudflared update or brew reinstall cloudflared"
417
+ ) from e
418
+ except Exception as e:
419
+ raise RuntimeError(
420
+ f"Failed to verify cloudflared binary: {e}. "
421
+ f"Binary path: {bin_path}. "
422
+ f"Try reinstalling: cloudflared update or brew reinstall cloudflared"
423
+ ) from e
424
+
425
+ # Capture stderr separately for better error diagnostics
426
+ # Use --config /dev/null to prevent loading any user config file
427
+ # This fixes issues where ~/.cloudflared/config.yml has ingress rules
428
+ # for named tunnels that interfere with quick tunnels (returning 404)
429
+ proc = subprocess.Popen(
430
+ [str(bin_path), "tunnel", "--config", "/dev/null", "--url", f"http://127.0.0.1:{port}"],
431
+ stdout=subprocess.PIPE,
432
+ stderr=subprocess.PIPE, # Capture stderr separately
433
+ text=True,
434
+ bufsize=1,
435
+ )
436
+
437
+ start = time.time()
438
+ url: Optional[str] = None
439
+ output_lines: list[str] = []
440
+ stderr_lines: list[str] = []
441
+
442
+ # Use select for non-blocking I/O to avoid hanging on readline()
443
+ import select
444
+
445
+ # Stream both stdout and stderr to detect the trycloudflare URL
446
+ # Note: cloudflared prints the URL to stderr, not stdout!
447
+ while time.time() - start < wait_s:
448
+ elapsed = time.time() - start
449
+ remaining_time = wait_s - elapsed
450
+
451
+ if remaining_time <= 0:
452
+ break
453
+
454
+ if proc.poll() is not None:
455
+ # Process exited early - try to read all available output
456
+ try:
457
+ stdout, stderr = proc.communicate(timeout=1.0)
458
+ except subprocess.TimeoutExpired:
459
+ proc.kill()
460
+ stdout, stderr = proc.communicate()
461
+
462
+ # Combine stdout and stderr for error message
463
+ all_output = ""
464
+ if stdout:
465
+ all_output += f"STDOUT:\n{stdout}\n"
466
+ if stderr:
467
+ all_output += f"STDERR:\n{stderr}\n"
468
+ if output_lines:
469
+ all_output += f"Captured stdout:\n{''.join(output_lines)}\n"
470
+ if stderr_lines:
471
+ all_output += f"Captured stderr:\n{''.join(stderr_lines)}\n"
472
+
473
+ # Check for rate limiting (429 Too Many Requests)
474
+ is_rate_limited = False
475
+ if stderr and "429" in stderr and "Too Many Requests" in stderr or stderr and "rate limit" in stderr.lower():
476
+ is_rate_limited = True
477
+
478
+ # Add diagnostic info
479
+ if is_rate_limited:
480
+ error_msg = (
481
+ "❌ RATE LIMIT ERROR: Cloudflare is blocking quick tunnel creation due to rate limiting.\n"
482
+ f"\n"
483
+ f"Error Details:\n"
484
+ f" • Exit code: {proc.returncode}\n"
485
+ f" • Status: 429 Too Many Requests\n"
486
+ f" • Command: {' '.join([str(bin_path), 'tunnel', '--url', f'http://127.0.0.1:{port}'])}\n"
487
+ f"\n"
488
+ f"Why this happens:\n"
489
+ f" Cloudflare limits how many quick (ephemeral) tunnels can be created\n"
490
+ f" in a short time period. You've hit this limit.\n"
491
+ f"\n"
492
+ f"Solutions (in order of preference):\n"
493
+ f" 1. ⏰ WAIT: Wait 5-10 minutes for the rate limit to reset\n"
494
+ f" 2. 🔑 USE MANAGED TUNNEL: Set SYNTH_API_KEY env var to use managed tunnels (no rate limits)\n"
495
+ f" 3. ♻️ REUSE EXISTING: Set INTERCEPTOR_TUNNEL_URL env var to reuse an existing tunnel\n"
496
+ f"\n"
497
+ f"Full error output:\n"
498
+ f"{all_output[:1000]}"
499
+ )
500
+ else:
501
+ error_msg = (
502
+ f"cloudflared exited early with code {proc.returncode}.\n"
503
+ f"Command: {' '.join([str(bin_path), 'tunnel', '--url', f'http://127.0.0.1:{port}'])}\n"
504
+ f"Binary path: {bin_path}\n"
505
+ )
506
+ if all_output:
507
+ error_msg += f"Output:\n{all_output[:1000]}"
508
+ else:
509
+ error_msg += "No output captured. This usually means:\n"
510
+ error_msg += " 1. cloudflared binary is corrupted or wrong architecture\n"
511
+ error_msg += " 2. cloudflared needs to be updated (try: cloudflared update)\n"
512
+ error_msg += " 3. System-level issue preventing cloudflared from running\n"
513
+ error_msg += " 4. Port conflict or network issue\n"
514
+ error_msg += f"\nTry running manually: {bin_path} tunnel --url http://127.0.0.1:{port}"
515
+
516
+ raise RuntimeError(error_msg)
517
+
518
+ # Read from both stdout and stderr (cloudflared prints URL to stderr!)
519
+ fds_to_check = []
520
+ from contextlib import suppress
521
+
522
+ if proc.stdout:
523
+ with suppress(ValueError, OSError):
524
+ fds_to_check.append(("stdout", proc.stdout.fileno(), proc.stdout))
525
+ if proc.stderr:
526
+ with suppress(ValueError, OSError):
527
+ fds_to_check.append(("stderr", proc.stderr.fileno(), proc.stderr))
528
+
529
+ if not fds_to_check:
530
+ if time.time() - start >= wait_s:
531
+ break
532
+ time.sleep(0.05)
533
+ continue
534
+
535
+ # Use select to check if data is available (non-blocking)
536
+ try:
537
+ fds = [fd for _, fd, _ in fds_to_check]
538
+ ready, _, _ = select.select(fds, [], [], min(0.1, remaining_time))
539
+
540
+ if ready:
541
+ # Check which file descriptors are ready
542
+ for name, fd, stream in fds_to_check:
543
+ if fd in ready:
544
+ # Data is available, read a line
545
+ line = stream.readline()
546
+ if line:
547
+ # Capture output for diagnostics
548
+ if name == "stdout":
549
+ output_lines.append(line)
550
+ else:
551
+ stderr_lines.append(line)
552
+
553
+ # Check current line for URL
554
+ match = _URL_RE.search(line)
555
+ if match:
556
+ url = match.group(0)
557
+ break
558
+
559
+ # Check for partial URL (truncated line) - wait for more data
560
+ partial_match = _URL_PARTIAL_RE.search(line)
561
+ if partial_match:
562
+ # Found partial URL, wait a bit longer for the rest
563
+ # Read more lines to get the complete URL
564
+ for _ in range(5): # Try reading up to 5 more lines
565
+ if time.time() - start >= wait_s:
566
+ break
567
+ time.sleep(0.1)
568
+ if proc.poll() is not None:
569
+ break
570
+ # Try to read more
571
+ if stream in [s for _, _, s in fds_to_check]:
572
+ try:
573
+ more_line = stream.readline()
574
+ if more_line:
575
+ if name == "stdout":
576
+ output_lines.append(more_line)
577
+ else:
578
+ stderr_lines.append(more_line)
579
+ line += more_line
580
+ except (OSError, ValueError):
581
+ pass
582
+
583
+ # Now check accumulated output for full URL
584
+ all_accumulated = ''.join(output_lines + stderr_lines)
585
+ match = _URL_RE.search(all_accumulated)
586
+ if match:
587
+ url = match.group(0)
588
+ break
589
+
590
+ # Also check accumulated output (URL might be split across lines)
591
+ all_accumulated = ''.join(output_lines + stderr_lines)
592
+ match = _URL_RE.search(all_accumulated)
593
+ if match:
594
+ url = match.group(0)
595
+ break
596
+
597
+ if url:
598
+ break
599
+ else:
600
+ # No data available, check timeout and continue
601
+ if time.time() - start >= wait_s:
602
+ break
603
+ time.sleep(0.05)
604
+ continue
605
+ except (ValueError, OSError) as e:
606
+ # File descriptor not available or select failed - fall back to reading both streams
607
+ # This can happen on Windows or if the file is closed
608
+ _ = e # Suppress unused variable warning
609
+ if proc.stdout:
610
+ line = proc.stdout.readline()
611
+ if line:
612
+ output_lines.append(line)
613
+ match = _URL_RE.search(line)
614
+ if match:
615
+ url = match.group(0)
616
+ break
617
+ # Check for partial URL
618
+ partial_match = _URL_PARTIAL_RE.search(line)
619
+ if partial_match:
620
+ # Wait a bit and read more
621
+ time.sleep(0.2)
622
+ more_line = proc.stdout.readline()
623
+ if more_line:
624
+ output_lines.append(more_line)
625
+ line += more_line
626
+ if proc.stderr:
627
+ line = proc.stderr.readline()
628
+ if line:
629
+ stderr_lines.append(line)
630
+ match = _URL_RE.search(line)
631
+ if match:
632
+ url = match.group(0)
633
+ break
634
+ # Check for partial URL
635
+ partial_match = _URL_PARTIAL_RE.search(line)
636
+ if partial_match:
637
+ # Wait a bit and read more
638
+ time.sleep(0.2)
639
+ more_line = proc.stderr.readline()
640
+ if more_line:
641
+ stderr_lines.append(more_line)
642
+ line += more_line
643
+
644
+ # Check accumulated output
645
+ all_accumulated = ''.join(output_lines + stderr_lines)
646
+ match = _URL_RE.search(all_accumulated)
647
+ if match:
648
+ url = match.group(0)
649
+ break
650
+
651
+ if time.time() - start >= wait_s:
652
+ break
653
+ time.sleep(0.05)
654
+ continue
655
+
656
+ if not url:
657
+ proc.terminate()
658
+ try:
659
+ stdout, stderr = proc.communicate(timeout=2.0)
660
+ except subprocess.TimeoutExpired:
661
+ proc.kill()
662
+ stdout, stderr = proc.communicate()
663
+
664
+ all_output = ""
665
+ if stdout:
666
+ all_output += f"STDOUT:\n{stdout}\n"
667
+ if stderr:
668
+ all_output += f"STDERR:\n{stderr}\n"
669
+ if output_lines:
670
+ all_output += f"Captured stdout:\n{''.join(output_lines)}\n"
671
+ if stderr_lines:
672
+ all_output += f"Captured stderr:\n{''.join(stderr_lines)}\n"
673
+
674
+ # Try to extract URL from accumulated output even if timeout occurred
675
+ all_accumulated = ''.join(output_lines + stderr_lines)
676
+ if stdout:
677
+ all_accumulated += stdout
678
+ if stderr:
679
+ all_accumulated += stderr
680
+
681
+ # Check for partial URL and try to reconstruct
682
+ if not url:
683
+ # Try first partial pattern (ends with trycloudf)
684
+ partial_match = _URL_PARTIAL_RE.search(all_accumulated)
685
+ if partial_match:
686
+ # Found partial URL - try to complete it
687
+ partial_url = partial_match.group(0)
688
+ # Partial match ends with "trycloudf", so we need "lare.com"
689
+ test_url = partial_url + "lare.com"
690
+ if _URL_RE.match(test_url):
691
+ url = test_url
692
+ logger.info(f"Reconstructed URL from partial match (trycloudf): {url}")
693
+
694
+ # Try second partial pattern (ends with tryclo)
695
+ if not url:
696
+ partial_match2 = _URL_PARTIAL_RE2.search(all_accumulated)
697
+ if partial_match2:
698
+ partial_url = partial_match2.group(0)
699
+ # Partial match ends with "tryclo", so we need "udflare.com"
700
+ test_url = partial_url + "udflare.com"
701
+ if _URL_RE.match(test_url):
702
+ url = test_url
703
+ logger.info(f"Reconstructed URL from partial match (tryclo): {url}")
704
+
705
+ if url:
706
+ return url, proc
707
+
708
+ error_msg = (
709
+ f"Failed to parse trycloudflare URL from cloudflared output after {wait_s}s.\n"
710
+ f"Command: {' '.join([str(bin_path), 'tunnel', '--url', f'http://127.0.0.1:{port}'])}\n"
711
+ )
712
+ if all_output:
713
+ error_msg += f"Output:\n{all_output[:1000]}"
714
+ else:
715
+ error_msg += "No output captured."
716
+
717
+ raise RuntimeError(error_msg)
718
+
719
+ return url, proc
720
+
721
+
722
+ async def resolve_hostname_with_explicit_resolvers(hostname: str) -> str:
723
+ """
724
+ Resolve hostname using explicit resolvers (1.1.1.1, 8.8.8.8) first,
725
+ then fall back to system resolver.
726
+
727
+ This fixes resolver path issues where system DNS is slow or blocking.
728
+
729
+ Args:
730
+ hostname: Hostname to resolve
731
+
732
+ Returns:
733
+ Resolved IP address
734
+
735
+ Raises:
736
+ socket.gaierror: If resolution fails with all resolvers
737
+ """
738
+ timeout = float(os.getenv("SYNTH_TUNNEL_DNS_TIMEOUT_PER_ATTEMPT_SECS", "5"))
739
+ loop = asyncio.get_event_loop()
740
+
741
+ # Try various dig resolvers, then fall back to system resolver
742
+ # Order: 1.1.1.1, 8.8.8.8, then plain dig (uses system's configured DNS but bypasses cache)
743
+ resolvers = [
744
+ ("1.1.1.1", ["dig", "@1.1.1.1", "+short", hostname]),
745
+ ("8.8.8.8", ["dig", "@8.8.8.8", "+short", hostname]),
746
+ ("default", ["dig", "+short", hostname]), # Plain dig bypasses negative cache
747
+ ]
748
+
749
+ for resolver_name, cmd in resolvers:
750
+ try:
751
+ result = await loop.run_in_executor(
752
+ None,
753
+ lambda c=cmd: subprocess.run(
754
+ c,
755
+ capture_output=True,
756
+ text=True,
757
+ timeout=timeout,
758
+ ),
759
+ )
760
+ if result.returncode == 0 and result.stdout.strip():
761
+ first = result.stdout.strip().splitlines()[0].strip()
762
+ if first:
763
+ logger.debug(f"Resolved via {resolver_name}: {hostname} -> {first}")
764
+ return first
765
+ except FileNotFoundError:
766
+ logger.debug(f"dig not found, skipping {resolver_name}")
767
+ continue
768
+ except Exception as e:
769
+ logger.debug(f"Resolver {resolver_name} failed: {e}")
770
+ continue
771
+
772
+ # Final fallback: system resolver (may hit negative cache)
773
+ logger.debug(f"Falling back to socket.gethostbyname for {hostname}")
774
+ return await loop.run_in_executor(
775
+ None,
776
+ socket.gethostbyname,
777
+ hostname,
778
+ )
779
+
780
+
781
+ async def verify_tunnel_dns_resolution(
782
+ tunnel_url: str,
783
+ name: str = "tunnel",
784
+ timeout_seconds: float = 60.0,
785
+ api_key: Optional[str] = None,
786
+ ) -> None:
787
+ """
788
+ Verify that a tunnel URL's hostname can be resolved via DNS (using public
789
+ resolvers first) and that HTTP connectivity works by connecting directly
790
+ to the resolved IP with the original Host header.
791
+
792
+ This avoids depending on the system resolver for HTTP checks, which was
793
+ causing [Errno 8] errors even after DNS resolved via explicit resolvers.
794
+
795
+ Args:
796
+ tunnel_url: The tunnel URL to verify (e.g., https://xxx.trycloudflare.com/v1)
797
+ name: Human-readable name for logging
798
+ timeout_seconds: Maximum time to wait for DNS resolution
799
+ api_key: Optional API key for health check authentication (defaults to ENVIRONMENT_API_KEY env var)
800
+
801
+ Raises:
802
+ RuntimeError: If DNS resolution or HTTP connectivity fails after timeout
803
+ """
804
+ parsed = urlparse(tunnel_url)
805
+ hostname = parsed.hostname
806
+ if not hostname:
807
+ logger.warning(f"No hostname in {name} tunnel URL: {tunnel_url}")
808
+ return
809
+
810
+ # Skip DNS check for localhost
811
+ if hostname in ("localhost", "127.0.0.1"):
812
+ logger.debug(f"Skipping DNS check for localhost {name}")
813
+ return
814
+
815
+ max_delay = 3.0
816
+ delay = 0.5
817
+ loop = asyncio.get_event_loop()
818
+ deadline = loop.time() + timeout_seconds
819
+ attempt = 0
820
+
821
+ logger.info(f"Verifying DNS resolution for {name}: {hostname} (timeout {timeout_seconds:.0f}s)...")
822
+
823
+ last_exc: Optional[Exception] = None
824
+
825
+ while True:
826
+ attempt += 1
827
+ try:
828
+ # 1. Resolve via explicit resolvers (1.1.1.1 / 8.8.8.8) → IP
829
+ resolved_ip = await resolve_hostname_with_explicit_resolvers(hostname)
830
+ logger.info(f"DNS resolution successful (attempt {attempt}): {hostname} -> {resolved_ip}")
831
+
832
+ # 2. HTTP connectivity: use curl with --resolve to bypass system DNS cache
833
+ # The system resolver may have negative-cached the hostname, so we use
834
+ # curl with explicit IP resolution to bypass it while maintaining proper SNI.
835
+ try:
836
+ scheme = parsed.scheme or "https"
837
+ test_url = f"{scheme}://{hostname}/health"
838
+ port = 443 if scheme == "https" else 80
839
+
840
+ # Build curl command with --resolve to bypass system DNS
841
+ # Format: --resolve hostname:port:ip
842
+ curl_cmd = [
843
+ "curl", "-s", "-o", "/dev/null", "-w", "%{http_code}",
844
+ "--max-time", "5",
845
+ "-k", # Allow self-signed certs
846
+ "--resolve", f"{hostname}:{port}:{resolved_ip}",
847
+ test_url,
848
+ ]
849
+
850
+ # Include API key if provided (or from env var)
851
+ if api_key is None:
852
+ # Try to load .env file if available
853
+ try:
854
+ from dotenv import load_dotenv
855
+ load_dotenv(override=False)
856
+ except ImportError:
857
+ pass
858
+ api_key = os.getenv("ENVIRONMENT_API_KEY")
859
+ if api_key:
860
+ curl_cmd.extend(["-H", f"X-API-Key: {api_key}"])
861
+
862
+ result = await loop.run_in_executor(
863
+ None,
864
+ lambda: subprocess.run(curl_cmd, capture_output=True, text=True, timeout=10),
865
+ )
866
+
867
+ status_code = int(result.stdout.strip()) if result.returncode == 0 and result.stdout.strip().isdigit() else 0
868
+
869
+ # Accept various status codes that indicate the tunnel is working:
870
+ # - 200: OK (service is running)
871
+ # - 400/401/403: Auth required (server is reachable)
872
+ # - 404/405: Not found / method not allowed (server is reachable)
873
+ # - 502: Bad gateway (cloudflared connected but local service isn't running)
874
+ if status_code in (200, 400, 401, 403, 404, 405, 502):
875
+ logger.info(f"HTTP connectivity verified: {test_url} -> {status_code}")
876
+ return
877
+ else:
878
+ # 530 errors are common when tunnel is still establishing - retry
879
+ if status_code == 530:
880
+ logger.debug("HTTP 530 (tunnel establishing) - will retry")
881
+ last_exc = RuntimeError("tunnel not ready yet (HTTP 530)")
882
+ elif result.returncode != 0:
883
+ logger.warning(f"curl failed: {result.stderr}")
884
+ last_exc = RuntimeError(f"curl failed: {result.stderr}")
885
+ else:
886
+ logger.warning(f"HTTP check returned unexpected status: {status_code}")
887
+ last_exc = RuntimeError(f"unexpected HTTP status {status_code}")
888
+ except Exception as http_exc:
889
+ logger.warning(f"HTTP connectivity check failed (attempt {attempt}): {http_exc}")
890
+ last_exc = http_exc
891
+
892
+ # DNS resolved, but HTTP check failed - wait and retry until deadline
893
+ now = loop.time()
894
+ if now >= deadline:
895
+ break
896
+ delay = min(delay * 2 if attempt > 1 else delay, max_delay)
897
+ sleep_for = min(delay, max(0.0, deadline - now))
898
+ logger.debug(f"Waiting {sleep_for:.1f}s before retry...")
899
+ await asyncio.sleep(sleep_for)
900
+
901
+ except socket.gaierror as e:
902
+ logger.warning(f"DNS resolution failed (attempt {attempt}): {e}")
903
+ last_exc = e
904
+ now = loop.time()
905
+ if now >= deadline:
906
+ raise RuntimeError(
907
+ f"DNS resolution failed for {name} tunnel hostname {hostname} "
908
+ f"after {timeout_seconds:.0f}s. Tunnel URL: {tunnel_url}. Error: {e}"
909
+ ) from e
910
+ delay = min(delay * 2 if attempt > 1 else delay, max_delay)
911
+ sleep_for = min(delay, max(0.0, deadline - now))
912
+ logger.debug(f"Waiting {sleep_for:.1f}s before retry...")
913
+ await asyncio.sleep(sleep_for)
914
+ except Exception as e:
915
+ logger.error(f"Unexpected error during DNS verification (attempt {attempt}): {e}")
916
+ last_exc = e
917
+ now = loop.time()
918
+ if now >= deadline:
919
+ raise RuntimeError(
920
+ f"DNS verification failed for {hostname} after {timeout_seconds:.0f}s: {e}"
921
+ ) from e
922
+ delay = min(delay * 2 if attempt > 1 else delay, max_delay)
923
+ sleep_for = min(delay, max(0.0, deadline - now))
924
+ await asyncio.sleep(sleep_for)
925
+
926
+ # If we get here, we ran out of time with HTTP still failing
927
+ raise RuntimeError(
928
+ f"DNS succeeded but HTTP connectivity could not be confirmed for {hostname} "
929
+ f"within {timeout_seconds:.0f}s. Last error: {last_exc}"
930
+ )
931
+
932
+
933
+ async def open_quick_tunnel_with_dns_verification(
934
+ port: int,
935
+ *,
936
+ wait_s: float = 10.0,
937
+ max_retries: Optional[int] = None,
938
+ dns_timeout_s: Optional[float] = None,
939
+ api_key: Optional[str] = None,
940
+ ) -> Tuple[str, subprocess.Popen]:
941
+ """
942
+ Open a quick Cloudflare tunnel with DNS verification and retry logic.
943
+
944
+ This wraps open_quick_tunnel with DNS verification to ensure the tunnel
945
+ is actually reachable before returning.
946
+
947
+ Args:
948
+ port: Local port to tunnel to
949
+ wait_s: Maximum time to wait for URL in seconds
950
+ max_retries: Maximum number of tunnel creation retries (default: from SYNTH_TUNNEL_MAX_RETRIES env var, or 2)
951
+ dns_timeout_s: Maximum time to wait for DNS resolution (default: from SYNTH_TUNNEL_DNS_TIMEOUT_SECS env var, or 60)
952
+ api_key: Optional API key for health check authentication (defaults to ENVIRONMENT_API_KEY env var)
953
+
954
+ Returns:
955
+ Tuple of (public_url, process_handle)
956
+
957
+ Raises:
958
+ RuntimeError: If tunnel creation or DNS verification fails after retries
959
+ """
960
+ max_retries = max_retries or int(os.getenv("SYNTH_TUNNEL_MAX_RETRIES", "2"))
961
+ dns_timeout_s = dns_timeout_s or float(os.getenv("SYNTH_TUNNEL_DNS_TIMEOUT_SECS", "60"))
962
+
963
+ # Get API key from parameter or env var
964
+ if api_key is None:
965
+ # Try to load .env file if available
966
+ try:
967
+ from dotenv import load_dotenv
968
+ load_dotenv(override=False)
969
+ except ImportError:
970
+ pass
971
+ api_key = os.getenv("ENVIRONMENT_API_KEY")
972
+
973
+ last_err: Optional[Exception] = None
974
+ for attempt in range(1, max_retries + 1):
975
+ proc: Optional[subprocess.Popen] = None
976
+ try:
977
+ logger.info(f"Tunnel attempt {attempt}/{max_retries}")
978
+ url, proc = open_quick_tunnel(port, wait_s=wait_s)
979
+ logger.info(f"Tunnel URL obtained: {url}")
980
+
981
+ # Give tunnel a moment to establish before verification
982
+ # Cloudflare tunnels can take a few seconds to become fully ready
983
+ logger.debug("Waiting 3s for tunnel to establish before verification...")
984
+ await asyncio.sleep(3.0)
985
+
986
+ # Verify DNS (this is where failures usually happen)
987
+ await verify_tunnel_dns_resolution(url, timeout_seconds=dns_timeout_s, name=f"tunnel attempt {attempt}", api_key=api_key)
988
+
989
+ logger.info("Tunnel verified and ready!")
990
+ return url, proc
991
+ except Exception as e:
992
+ last_err = e
993
+ # Check if this is a rate limit error and make it clearer
994
+ error_str = str(e)
995
+ is_rate_limit = "429" in error_str and "Too Many Requests" in error_str
996
+ if is_rate_limit:
997
+ logger.error(
998
+ f"❌ RATE LIMIT: Tunnel attempt {attempt}/{max_retries} failed due to Cloudflare rate limiting. "
999
+ f"This means too many quick tunnels were created recently. "
1000
+ f"Wait 5-10 minutes or use managed tunnels (set SYNTH_API_KEY)."
1001
+ )
1002
+ else:
1003
+ logger.warning(f"Tunnel attempt {attempt} failed: {e}")
1004
+ if proc is not None and proc.poll() is None:
1005
+ proc.terminate()
1006
+ try:
1007
+ proc.wait(timeout=5.0)
1008
+ except subprocess.TimeoutExpired:
1009
+ proc.kill()
1010
+ if attempt < max_retries:
1011
+ logger.info("Retrying after 10s backoff...")
1012
+ await asyncio.sleep(10.0)
1013
+ else:
1014
+ break
1015
+
1016
+ assert last_err is not None
1017
+ raise last_err
1018
+
1019
+
1020
+ async def check_rate_limit_status(test_port: int = 19999) -> dict[str, Any]:
1021
+ """
1022
+ Check if Cloudflare is currently rate-limiting quick tunnel creation.
1023
+
1024
+ This attempts to create a quick tunnel and checks for rate limit errors.
1025
+
1026
+ Args:
1027
+ test_port: Port to use for test tunnel (should be available)
1028
+
1029
+ Returns:
1030
+ dict with keys:
1031
+ - is_rate_limited: bool
1032
+ - exit_code: int | None
1033
+ - error_message: str | None
1034
+ - output: str
1035
+ """
1036
+ import http.server
1037
+ import socketserver
1038
+ import threading
1039
+
1040
+ bin_path = require_cloudflared()
1041
+
1042
+ # Start a dummy HTTP server
1043
+ server = None
1044
+ server_thread = None
1045
+
1046
+ try:
1047
+ handler = http.server.SimpleHTTPRequestHandler
1048
+ server = socketserver.TCPServer(("127.0.0.1", test_port), handler)
1049
+ server.allow_reuse_address = True
1050
+ server_thread = threading.Thread(target=server.serve_forever, daemon=True)
1051
+ server_thread.start()
1052
+ await asyncio.sleep(0.5)
1053
+
1054
+ # Try to create a tunnel (use --config /dev/null to ignore user config)
1055
+ proc = subprocess.Popen(
1056
+ [str(bin_path), "tunnel", "--config", "/dev/null", "--url", f"http://127.0.0.1:{test_port}"],
1057
+ stdout=subprocess.PIPE,
1058
+ stderr=subprocess.PIPE,
1059
+ text=True,
1060
+ )
1061
+
1062
+ # Wait a few seconds
1063
+ start = time.time()
1064
+ output_lines = []
1065
+ stderr_lines = []
1066
+
1067
+ while time.time() - start < 3.0:
1068
+ if proc.poll() is not None:
1069
+ stdout, stderr = proc.communicate()
1070
+ if stdout:
1071
+ output_lines.extend(stdout.splitlines())
1072
+ if stderr:
1073
+ stderr_lines.extend(stderr.splitlines())
1074
+ break
1075
+ await asyncio.sleep(0.1)
1076
+
1077
+ # Clean up
1078
+ proc.terminate()
1079
+ try:
1080
+ proc.wait(timeout=2.0)
1081
+ except subprocess.TimeoutExpired:
1082
+ proc.kill()
1083
+
1084
+ all_output = "\n".join(stderr_lines + output_lines)
1085
+
1086
+ # Check for rate limit
1087
+ is_rate_limited = False
1088
+ if proc.returncode == 1 and (
1089
+ ("429" in all_output and "Too Many Requests" in all_output) or "rate limit" in all_output.lower()
1090
+ ):
1091
+ is_rate_limited = True
1092
+
1093
+ return {
1094
+ "is_rate_limited": is_rate_limited,
1095
+ "exit_code": proc.returncode,
1096
+ "error_message": all_output if is_rate_limited else None,
1097
+ "output": all_output,
1098
+ }
1099
+
1100
+ finally:
1101
+ if server:
1102
+ server.shutdown()
1103
+ server.server_close()
1104
+ if server_thread:
1105
+ server_thread.join(timeout=2.0)
1106
+
1107
+
1108
+ def open_managed_tunnel(tunnel_token: str) -> subprocess.Popen:
1109
+ """
1110
+ Open a managed (named) Cloudflare tunnel using a token.
1111
+
1112
+ Args:
1113
+ tunnel_token: Cloudflare tunnel token from backend API
1114
+
1115
+ Returns:
1116
+ Process handle for the tunnel
1117
+
1118
+ Raises:
1119
+ RuntimeError: If cloudflared is not installed
1120
+ """
1121
+ bin_path = require_cloudflared()
1122
+ # cloudflared v2023.4+ accepts --token for named tunnels
1123
+ return subprocess.Popen(
1124
+ [str(bin_path), "tunnel", "run", "--token", tunnel_token],
1125
+ stdout=subprocess.PIPE,
1126
+ stderr=subprocess.STDOUT,
1127
+ text=True,
1128
+ bufsize=1,
1129
+ )
1130
+
1131
+
1132
+ async def wait_for_cloudflared_connection(
1133
+ proc: subprocess.Popen,
1134
+ timeout_seconds: float = 30.0,
1135
+ ) -> bool:
1136
+ """
1137
+ Wait for cloudflared to establish a connection to Cloudflare's edge.
1138
+
1139
+ This monitors cloudflared's stdout/stderr for connection success messages.
1140
+ DNS records only resolve AFTER cloudflared has connected, so this function
1141
+ must be called before attempting DNS verification.
1142
+
1143
+ Args:
1144
+ proc: The cloudflared subprocess from open_managed_tunnel()
1145
+ timeout_seconds: Maximum time to wait for connection
1146
+
1147
+ Returns:
1148
+ True if connection was established, False if timeout or error
1149
+
1150
+ Raises:
1151
+ RuntimeError: If cloudflared exits with an error before connecting
1152
+ """
1153
+ import select
1154
+
1155
+ # Patterns that indicate successful connection
1156
+ # cloudflared outputs: "INF Registered tunnel connection connIndex=0 connection=..."
1157
+ # We need to be specific - "connIndex=" alone triggers too early on curve preferences log
1158
+ connection_patterns = [
1159
+ "Registered tunnel connection",
1160
+ "Connection registered",
1161
+ # Don't use "connIndex=" alone - it matches curve preferences log before actual connection
1162
+ ]
1163
+
1164
+ # Patterns that indicate fatal errors
1165
+ error_patterns = [
1166
+ "failed to connect",
1167
+ "error connecting",
1168
+ "tunnel credentials",
1169
+ "invalid token",
1170
+ "tunnel not found",
1171
+ "unauthorized",
1172
+ ]
1173
+
1174
+ loop = asyncio.get_event_loop()
1175
+ start_time = loop.time()
1176
+ output_lines: list[str] = []
1177
+
1178
+ logger.info(f"Waiting for cloudflared to connect (timeout {timeout_seconds}s)...")
1179
+
1180
+ while True:
1181
+ elapsed = loop.time() - start_time
1182
+ if elapsed >= timeout_seconds:
1183
+ logger.warning(
1184
+ f"cloudflared connection timeout after {elapsed:.1f}s. "
1185
+ f"Output: {' | '.join(output_lines[-10:])}"
1186
+ )
1187
+ return False
1188
+
1189
+ # Check if process exited
1190
+ if proc.poll() is not None:
1191
+ # Process exited - read remaining output
1192
+ remaining = proc.stdout.read() if proc.stdout else ""
1193
+ if remaining:
1194
+ output_lines.extend(remaining.splitlines())
1195
+
1196
+ all_output = "\n".join(output_lines)
1197
+ logger.error(
1198
+ f"cloudflared exited with code {proc.returncode} before connecting. "
1199
+ f"Output:\n{all_output[:2000]}"
1200
+ )
1201
+ raise RuntimeError(
1202
+ f"cloudflared exited with code {proc.returncode} before establishing connection. "
1203
+ f"This usually means the tunnel token is invalid or the tunnel was deleted. "
1204
+ f"Output: {all_output[:500]}"
1205
+ )
1206
+
1207
+ # Try to read output (non-blocking)
1208
+ if proc.stdout:
1209
+ try:
1210
+ # Use select for non-blocking read
1211
+ ready, _, _ = select.select([proc.stdout], [], [], 0.1)
1212
+ if ready:
1213
+ line = proc.stdout.readline()
1214
+ if line:
1215
+ line = line.strip()
1216
+ output_lines.append(line)
1217
+ logger.debug(f"cloudflared: {line}")
1218
+
1219
+ # Check for connection success
1220
+ line_lower = line.lower()
1221
+ for pattern in connection_patterns:
1222
+ if pattern.lower() in line_lower:
1223
+ logger.info(
1224
+ f"cloudflared connected after {elapsed:.1f}s: {line}"
1225
+ )
1226
+ return True
1227
+
1228
+ # Check for fatal errors
1229
+ for pattern in error_patterns:
1230
+ if pattern.lower() in line_lower:
1231
+ logger.error(f"cloudflared error detected: {line}")
1232
+ raise RuntimeError(
1233
+ f"cloudflared connection failed: {line}"
1234
+ )
1235
+ except (ValueError, OSError) as e:
1236
+ logger.debug(f"Error reading cloudflared output: {e}")
1237
+
1238
+ # Small sleep to avoid busy loop
1239
+ await asyncio.sleep(0.1)
1240
+
1241
+
1242
+ async def open_managed_tunnel_with_connection_wait(
1243
+ tunnel_token: str,
1244
+ timeout_seconds: float = 30.0,
1245
+ ) -> subprocess.Popen:
1246
+ """
1247
+ Open a managed tunnel and wait for cloudflared to connect.
1248
+
1249
+ This is the preferred method for starting managed tunnels as it ensures
1250
+ cloudflared has actually connected to Cloudflare's edge before returning.
1251
+ DNS records only resolve after this connection is established.
1252
+
1253
+ Args:
1254
+ tunnel_token: Cloudflare tunnel token from backend API
1255
+ timeout_seconds: Maximum time to wait for connection
1256
+
1257
+ Returns:
1258
+ Process handle for the connected tunnel
1259
+
1260
+ Raises:
1261
+ RuntimeError: If cloudflared fails to connect within timeout
1262
+ """
1263
+ proc = open_managed_tunnel(tunnel_token)
1264
+
1265
+ try:
1266
+ connected = await wait_for_cloudflared_connection(proc, timeout_seconds)
1267
+ if not connected:
1268
+ # Timeout - kill process and raise
1269
+ stop_tunnel(proc)
1270
+ raise RuntimeError(
1271
+ f"cloudflared failed to connect within {timeout_seconds}s. "
1272
+ "The tunnel may be invalid or Cloudflare may be experiencing issues."
1273
+ )
1274
+ return proc
1275
+ except Exception:
1276
+ # Cleanup on any error
1277
+ stop_tunnel(proc)
1278
+ raise
1279
+
1280
+
1281
+ def stop_tunnel(proc: Optional[subprocess.Popen]) -> None:
1282
+ """
1283
+ Gracefully stop a tunnel process.
1284
+
1285
+ Args:
1286
+ proc: Process handle to terminate, or None
1287
+ """
1288
+ if proc is None:
1289
+ return
1290
+
1291
+ if proc.poll() is None:
1292
+ # Process is still running
1293
+ proc.terminate()
1294
+ try:
1295
+ proc.wait(timeout=5.0)
1296
+ except subprocess.TimeoutExpired:
1297
+ # Force kill if graceful termination fails
1298
+ proc.kill()
1299
+ proc.wait()
1300
+
1301
+
1302
+ def store_tunnel_credentials(
1303
+ tunnel_url: str,
1304
+ access_client_id: Optional[str] = None,
1305
+ access_client_secret: Optional[str] = None,
1306
+ env_file: Optional[Path] = None,
1307
+ ) -> None:
1308
+ """
1309
+ Store tunnel credentials in .env file for optimizer to use.
1310
+
1311
+ Writes:
1312
+ - TASK_APP_URL=<tunnel_url>
1313
+ - CF_ACCESS_CLIENT_ID=<client_id> (if Access enabled)
1314
+ - CF_ACCESS_CLIENT_SECRET=<client_secret> (if Access enabled)
1315
+
1316
+ Args:
1317
+ tunnel_url: Public tunnel URL (e.g., "https://cust-abc123.usesynth.ai")
1318
+ access_client_id: Cloudflare Access client ID (optional)
1319
+ access_client_secret: Cloudflare Access client secret (optional)
1320
+ env_file: Path to .env file (defaults to .env in current directory)
1321
+ """
1322
+ __write_env_var_to_dotenv(
1323
+ "TASK_APP_URL",
1324
+ tunnel_url,
1325
+ output_file_path=env_file,
1326
+ print_msg=True,
1327
+ mask_msg=False,
1328
+ )
1329
+
1330
+ if access_client_id:
1331
+ __write_env_var_to_dotenv(
1332
+ "CF_ACCESS_CLIENT_ID",
1333
+ access_client_id,
1334
+ output_file_path=env_file,
1335
+ print_msg=True,
1336
+ mask_msg=True,
1337
+ )
1338
+
1339
+ if access_client_secret:
1340
+ __write_env_var_to_dotenv(
1341
+ "CF_ACCESS_CLIENT_SECRET",
1342
+ access_client_secret,
1343
+ output_file_path=env_file,
1344
+ print_msg=True,
1345
+ mask_msg=True,
1346
+ )
1347
+
1348
+
1349
+ # ---------------------------------------------------------------------------
1350
+ # Tunnel deployment helpers
1351
+ # ---------------------------------------------------------------------------
1352
+
1353
+
1354
+ async def rotate_tunnel(
1355
+ synth_api_key: str,
1356
+ port: int,
1357
+ reason: Optional[str] = None,
1358
+ backend_url: Optional[str] = None,
1359
+ ) -> dict[str, Any]:
1360
+ """
1361
+ Rotate (delete + recreate) the org's managed tunnel via Synth backend API.
1362
+
1363
+ This is useful when a tunnel becomes stale or inaccessible. It will:
1364
+ 1. Delete any existing active tunnels for the org
1365
+ 2. Create a fresh tunnel with a new auto-generated subdomain
1366
+ 3. Wait for DNS propagation (up to 90s) before returning
1367
+
1368
+ Args:
1369
+ synth_api_key: Synth API key for authentication
1370
+ port: Local port the new tunnel will forward to
1371
+ reason: Optional reason for rotation (for logging)
1372
+ backend_url: Optional backend URL (defaults to get_backend_url())
1373
+
1374
+ Returns:
1375
+ Dict containing:
1376
+ - tunnel_token: Token for cloudflared
1377
+ - hostname: Public hostname (e.g., "task-8114-12345.usesynth.ai")
1378
+ - access_client_id: Cloudflare Access client ID (if Access enabled)
1379
+ - access_client_secret: Cloudflare Access client secret (if Access enabled)
1380
+ - dns_verified: True if backend verified DNS propagation (SDK can skip DNS verification)
1381
+ - metadata: Dict with dns_verified and dns_verified_at timestamp
1382
+
1383
+ Raises:
1384
+ RuntimeError: If API request fails
1385
+ """
1386
+ from synth_ai.core.env import get_backend_url
1387
+
1388
+ base_url = backend_url or get_backend_url()
1389
+ url = f"{base_url}/api/v1/tunnels/rotate"
1390
+
1391
+ def mask_key(key: str) -> str:
1392
+ if len(key) > 14:
1393
+ return f"{key[:10]}...{key[-4:]}"
1394
+ return f"{key[:6]}..."
1395
+
1396
+ try:
1397
+ # Backend now waits up to 90s for DNS propagation, so we need a longer timeout
1398
+ async with httpx.AsyncClient(timeout=180.0, follow_redirects=True) as client:
1399
+ response = await client.post(
1400
+ url,
1401
+ headers={
1402
+ "X-API-Key": synth_api_key,
1403
+ "Authorization": f"Bearer {synth_api_key}",
1404
+ },
1405
+ json={
1406
+ "local_port": port,
1407
+ "local_host": "127.0.0.1",
1408
+ "reason": reason,
1409
+ },
1410
+ )
1411
+ response.raise_for_status()
1412
+ return response.json()
1413
+ except httpx.HTTPStatusError as exc:
1414
+ error_detail = exc.response.text
1415
+ try:
1416
+ import json
1417
+ error_json = json.loads(error_detail)
1418
+ error_detail = str(error_json.get("detail", error_detail))
1419
+ except Exception:
1420
+ pass
1421
+
1422
+ raise RuntimeError(
1423
+ f"Backend API returned {exc.response.status_code} when rotating tunnel:\n"
1424
+ f" Error: {error_detail}\n"
1425
+ f" URL: {url}\n"
1426
+ f" API Key: {mask_key(synth_api_key)}"
1427
+ ) from exc
1428
+ except httpx.ReadTimeout as exc:
1429
+ raise RuntimeError(
1430
+ f"Request timed out when rotating tunnel (backend waits for DNS propagation):\n"
1431
+ f" URL: {url}\n"
1432
+ f" Timeout: 180s\n"
1433
+ f" This is usually temporary - try again in a moment"
1434
+ ) from exc
1435
+ except httpx.RequestError as exc:
1436
+ raise RuntimeError(
1437
+ f"Failed to connect to backend when rotating tunnel:\n"
1438
+ f" URL: {url}\n"
1439
+ f" Error: {exc}"
1440
+ ) from exc
1441
+
1442
+
1443
+ async def create_tunnel(
1444
+ synth_api_key: str,
1445
+ port: int,
1446
+ subdomain: Optional[str] = None,
1447
+ ) -> dict[str, Any]:
1448
+ """
1449
+ Create a managed Cloudflare tunnel via Synth backend API.
1450
+
1451
+ The backend waits for DNS propagation (up to 90s) before returning.
1452
+
1453
+ Args:
1454
+ synth_api_key: Synth API key for authentication
1455
+ port: Local port the tunnel will forward to
1456
+ subdomain: Optional custom subdomain (e.g., "my-company")
1457
+
1458
+ Returns:
1459
+ Dict containing:
1460
+ - tunnel_token: Token for cloudflared
1461
+ - hostname: Public hostname (e.g., "cust-abc123.usesynth.ai")
1462
+ - access_client_id: Cloudflare Access client ID (if Access enabled)
1463
+ - access_client_secret: Cloudflare Access client secret (if Access enabled)
1464
+ - dns_verified: True if backend verified DNS propagation (SDK can skip DNS verification)
1465
+ - metadata: Dict with dns_verified and dns_verified_at timestamp
1466
+
1467
+ Raises:
1468
+ RuntimeError: If API request fails
1469
+ """
1470
+ url = f"{BACKEND_URL_BASE}/api/v1/tunnels/"
1471
+
1472
+ # Mask API key for error messages
1473
+ def mask_key(key: str) -> str:
1474
+ if len(key) > 14:
1475
+ return f"{key[:10]}...{key[-4:]}"
1476
+ return f"{key[:6]}..."
1477
+
1478
+ try:
1479
+ # Use X-API-Key header (backend expects this format)
1480
+ # Also support Authorization header as fallback
1481
+ # Backend now waits up to 90s for DNS propagation, so we need a longer timeout
1482
+ async with httpx.AsyncClient(timeout=180.0, follow_redirects=True) as client:
1483
+ response = await client.post(
1484
+ url,
1485
+ headers={
1486
+ "X-API-Key": synth_api_key,
1487
+ "Authorization": f"Bearer {synth_api_key}", # Fallback
1488
+ },
1489
+ json={
1490
+ "subdomain": subdomain or f"tunnel-{port}",
1491
+ "local_port": port,
1492
+ "local_host": "127.0.0.1",
1493
+ },
1494
+ )
1495
+ response.raise_for_status()
1496
+ return response.json()
1497
+ except httpx.HTTPStatusError as exc:
1498
+ error_detail = exc.response.text
1499
+ try:
1500
+ import json
1501
+ error_json = json.loads(error_detail)
1502
+ error_detail = str(error_json.get("detail", error_detail))
1503
+ except Exception:
1504
+ pass
1505
+
1506
+ # Provide helpful error message
1507
+ if exc.response.status_code == 401:
1508
+ raise RuntimeError(
1509
+ f"Authentication failed when creating tunnel:\n"
1510
+ f" Status: {exc.response.status_code}\n"
1511
+ f" Error: {error_detail}\n"
1512
+ f" API Key used: {mask_key(synth_api_key)}\n"
1513
+ f" URL: {url}\n"
1514
+ f" This usually means:\n"
1515
+ f" - The API key is invalid or expired\n"
1516
+ f" - The backend is experiencing high load (PostgREST timeout)\n"
1517
+ f" - Network connectivity issues\n"
1518
+ f" Try:\n"
1519
+ f" - Verify SYNTH_API_KEY is set correctly\n"
1520
+ f" - Wait a moment and retry (backend may be under load)\n"
1521
+ f" - Use tunnel_mode='quick' as a workaround"
1522
+ ) from exc
1523
+ else:
1524
+ raise RuntimeError(
1525
+ f"Backend API returned {exc.response.status_code} when creating tunnel:\n"
1526
+ f" Error: {error_detail}\n"
1527
+ f" URL: {url}\n"
1528
+ f" API Key: {mask_key(synth_api_key)}"
1529
+ ) from exc
1530
+ except httpx.ReadTimeout as exc:
1531
+ raise RuntimeError(
1532
+ f"Request timed out when creating tunnel (backend waits for DNS propagation):\n"
1533
+ f" URL: {url}\n"
1534
+ f" API Key: {mask_key(synth_api_key)}\n"
1535
+ f" Timeout: 180s\n"
1536
+ f" This is usually temporary - try again in a moment"
1537
+ ) from exc
1538
+ except httpx.RequestError as exc:
1539
+ raise RuntimeError(
1540
+ f"Failed to connect to backend when creating tunnel:\n"
1541
+ f" URL: {url}\n"
1542
+ f" API Key: {mask_key(synth_api_key)}\n"
1543
+ f" Error: {exc}\n"
1544
+ f" Check network connectivity and backend availability"
1545
+ ) from exc
1546
+
1547
+
1548
+ async def wait_for_health_check(
1549
+ host: str,
1550
+ port: int,
1551
+ api_key: str,
1552
+ timeout: float = 30.0,
1553
+ ) -> None:
1554
+ """
1555
+ Wait for task app health endpoint to be ready.
1556
+
1557
+ Args:
1558
+ host: Host to check
1559
+ port: Port to check
1560
+ api_key: API key for authentication
1561
+ timeout: Maximum time to wait in seconds
1562
+
1563
+ Raises:
1564
+ RuntimeError: If health check fails or times out
1565
+ """
1566
+ health_url = f"http://{host}:{port}/health"
1567
+ headers = {"X-API-Key": api_key}
1568
+ start = time.time()
1569
+
1570
+ while time.time() - start < timeout:
1571
+ try:
1572
+ async with httpx.AsyncClient(timeout=5.0) as client:
1573
+ response = await client.get(health_url, headers=headers)
1574
+ # Accept both 200 (success) and 400 (auth error means server is up)
1575
+ if response.status_code in (200, 400):
1576
+ return
1577
+ except (httpx.RequestError, httpx.TimeoutException):
1578
+ pass
1579
+
1580
+ await asyncio.sleep(0.5)
1581
+
1582
+ raise RuntimeError(
1583
+ f"Health check failed: {health_url} not ready after {timeout}s. "
1584
+ "Make sure your task app has a /health endpoint."
1585
+ )
1586
+
1587
+
1588
+ def _start_uvicorn_background(
1589
+ app: ASGIApp,
1590
+ host: str,
1591
+ port: int,
1592
+ daemon: bool = True,
1593
+ ) -> None:
1594
+ """
1595
+ Start uvicorn server in a background thread.
1596
+
1597
+ Args:
1598
+ app: ASGI application
1599
+ host: Host to bind to
1600
+ port: Port to bind to
1601
+ daemon: If True, thread dies when main process exits. If False, thread keeps running.
1602
+ """
1603
+ import threading
1604
+
1605
+ def serve():
1606
+ try:
1607
+ uvicorn.run(
1608
+ app,
1609
+ host=host,
1610
+ port=port,
1611
+ reload=False,
1612
+ log_level="info",
1613
+ )
1614
+ except Exception as exc:
1615
+ # Log error but don't raise (background thread)
1616
+ print(f"Uvicorn error: {exc}", flush=True)
1617
+
1618
+ thread = threading.Thread(
1619
+ target=serve,
1620
+ name=f"synth-uvicorn-tunnel-{port}",
1621
+ daemon=daemon,
1622
+ )
1623
+ thread.start()
1624
+
1625
+
1626
+ async def deploy_app_tunnel(
1627
+ cfg: CFDeployCfg,
1628
+ env_file: Optional[Path] = None,
1629
+ keep_alive: bool = False,
1630
+ wait: bool = False,
1631
+ health_check_timeout: float = 30.0,
1632
+ ) -> str:
1633
+ """
1634
+ Deploy task app via Cloudflare Tunnel.
1635
+
1636
+ This function provides a clean abstraction that handles:
1637
+ 1. Starting the local task app (uvicorn) in background
1638
+ 2. Optionally waiting for health check (only if wait=True)
1639
+ 3. Opening tunnel (quick or managed)
1640
+ 4. Writing tunnel URL and Access credentials to .env
1641
+ 5. Optionally keeping processes alive (blocking vs non-blocking mode)
1642
+
1643
+ By default (wait=False), this function is non-blocking and returns immediately
1644
+ after starting the tunnel. This is designed for AI agent use to prevent indefinite stalls.
1645
+ Processes run in the background and will continue until explicitly stopped.
1646
+
1647
+ When `wait=True` or `keep_alive=True`, this function blocks and keeps the tunnel running
1648
+ until interrupted (Ctrl+C). Use this for interactive use or when you need to wait
1649
+ for the deployment to complete.
1650
+
1651
+ Args:
1652
+ cfg: Tunnel deployment configuration
1653
+ env_file: Optional path to .env file (defaults to .env in current directory)
1654
+ keep_alive: (Deprecated) If True, block and keep tunnel alive until interrupted.
1655
+ Use `wait` instead.
1656
+ wait: If True, wait for health check and block until interrupted.
1657
+ If False (default), return immediately after deployment (background mode).
1658
+ health_check_timeout: Maximum time to wait for health check (only used if wait=True)
1659
+
1660
+ Returns:
1661
+ Public tunnel URL
1662
+
1663
+ Raises:
1664
+ RuntimeError: If deployment fails at any step
1665
+
1666
+ Example:
1667
+ # Non-blocking (background mode, returns immediately) - DEFAULT
1668
+ url = await deploy_app_tunnel(cfg, wait=False)
1669
+
1670
+ # Blocking (waits for health check and keeps tunnel alive)
1671
+ url = await deploy_app_tunnel(cfg, wait=True)
1672
+ """
1673
+ ctx: dict[str, Any] = {
1674
+ "mode": cfg.mode,
1675
+ "host": cfg.host,
1676
+ "port": cfg.port,
1677
+ "task_app_path": str(cfg.task_app_path) if cfg.task_app_path else None,
1678
+ "wait": wait,
1679
+ }
1680
+ log_info("deploy_app_tunnel invoked", ctx=ctx)
1681
+
1682
+ ensure_cloudflared_installed()
1683
+
1684
+ selected_managed: Optional[ManagedTunnelRecord] = None
1685
+ synth_api_key: Optional[str] = None
1686
+
1687
+ if cfg.mode == "managed":
1688
+ synth_api_key = __resolve_env_var("SYNTH_API_KEY")
1689
+ tunnels = await fetch_managed_tunnels(synth_api_key)
1690
+ if tunnels:
1691
+ selected_managed = _select_existing_tunnel(tunnels, cfg.subdomain)
1692
+ if selected_managed:
1693
+ cfg.host = selected_managed.local_host or cfg.host
1694
+ cfg.port = selected_managed.local_port or cfg.port
1695
+ else:
1696
+ print("ℹ️ No managed tunnels found; provisioning a new managed tunnel.")
1697
+
1698
+ # Load environment variables from env_file before starting uvicorn
1699
+ # This ensures all env vars (HF cache paths, dataset names, etc.) are available to the task app
1700
+ if env_file and env_file.exists():
1701
+ try:
1702
+ from dotenv import load_dotenv
1703
+ load_dotenv(str(env_file), override=True)
1704
+ # Also explicitly set critical env vars to ensure they're available
1705
+ # Read the file directly to set vars even if dotenv fails
1706
+ try:
1707
+ with open(env_file) as f:
1708
+ for line in f:
1709
+ line = line.strip()
1710
+ if line and not line.startswith("#") and "=" in line:
1711
+ key, value = line.split("=", 1)
1712
+ # Remove quotes if present
1713
+ value = value.strip().strip('"').strip("'")
1714
+ os.environ[key.strip()] = value
1715
+ except Exception as file_exc:
1716
+ logger.debug(f"Could not read env_file directly: {file_exc}")
1717
+ logger.debug(f"Loaded environment from {env_file}")
1718
+ except ImportError:
1719
+ logger.warning("python-dotenv not available, skipping env_file load")
1720
+ # Fallback: read file directly
1721
+ try:
1722
+ with open(env_file) as f:
1723
+ for line in f:
1724
+ line = line.strip()
1725
+ if line and not line.startswith("#") and "=" in line:
1726
+ key, value = line.split("=", 1)
1727
+ value = value.strip().strip('"').strip("'")
1728
+ os.environ[key.strip()] = value
1729
+ except Exception as file_exc:
1730
+ logger.warning(f"Failed to read env_file directly: {file_exc}")
1731
+ except Exception as exc:
1732
+ logger.warning(f"Failed to load env_file {env_file}: {exc}")
1733
+
1734
+ os.environ["ENVIRONMENT_API_KEY"] = cfg.env_api_key
1735
+ if cfg.trace:
1736
+ os.environ["TASKAPP_TRACING_ENABLED"] = "1"
1737
+ else:
1738
+ os.environ.pop("TASKAPP_TRACING_ENABLED", None)
1739
+
1740
+ configure_import_paths(cfg.task_app_path, REPO_ROOT)
1741
+ module = load_module(cfg.task_app_path, f"_synth_tunnel_task_app_{cfg.task_app_path.stem}")
1742
+ app = get_asgi_app(module)
1743
+
1744
+ # Always use non-daemon thread so it survives when main process exits
1745
+ _start_uvicorn_background(app, cfg.host, cfg.port, daemon=False)
1746
+
1747
+ # Only wait for health check if wait mode is enabled (for AI agents, skip to avoid stalls)
1748
+ if wait or keep_alive:
1749
+ await wait_for_health_check(cfg.host, cfg.port, cfg.env_api_key, timeout=health_check_timeout)
1750
+ else:
1751
+ # In background mode, give it a short moment to start, but don't wait for full health check
1752
+ # This prevents indefinite stalls while still allowing the server to start
1753
+ import asyncio
1754
+ await asyncio.sleep(1.0) # Brief delay to let server start
1755
+
1756
+ tunnel_proc: Optional[subprocess.Popen] = None
1757
+ try:
1758
+ if cfg.mode == "quick":
1759
+ # Quick tunnel: ephemeral, no backend API call
1760
+ url, tunnel_proc = open_quick_tunnel(cfg.port)
1761
+ _TUNNEL_PROCESSES[cfg.port] = tunnel_proc
1762
+ store_tunnel_credentials(url, None, None, env_file)
1763
+ # Record tunnel for scan command
1764
+ try:
1765
+ from synth_ai.cli.lib.tunnel_records import record_tunnel
1766
+ record_tunnel(
1767
+ url=url,
1768
+ port=cfg.port,
1769
+ mode="quick",
1770
+ pid=tunnel_proc.pid if tunnel_proc else None,
1771
+ hostname=url.replace("https://", "").split("/")[0] if url.startswith("https://") else None,
1772
+ local_host=cfg.host,
1773
+ task_app_path=str(cfg.task_app_path) if cfg.task_app_path else None,
1774
+ )
1775
+ except Exception:
1776
+ pass # Fail silently - records are optional
1777
+ else:
1778
+ # Managed tunnel: either reuse or provision via backend API
1779
+ if selected_managed:
1780
+ tunnel_token = selected_managed.credential("tunnel_token")
1781
+ if not tunnel_token:
1782
+ raise RuntimeError(
1783
+ "Managed tunnel metadata missing tunnel_token. "
1784
+ "Delete the tunnel or contact Synth support."
1785
+ )
1786
+ hostname = selected_managed.hostname
1787
+ access_client_id = selected_managed.credential("access_client_id")
1788
+ access_client_secret = selected_managed.credential("access_client_secret")
1789
+ else:
1790
+ if not synth_api_key:
1791
+ synth_api_key = __resolve_env_var("SYNTH_API_KEY")
1792
+ data = await create_tunnel(synth_api_key, cfg.port, cfg.subdomain)
1793
+ tunnel_token = data["tunnel_token"]
1794
+ hostname = data["hostname"]
1795
+ access_client_id = data.get("access_client_id")
1796
+ access_client_secret = data.get("access_client_secret")
1797
+
1798
+ tunnel_proc = open_managed_tunnel(str(tunnel_token))
1799
+ _TUNNEL_PROCESSES[cfg.port] = tunnel_proc
1800
+
1801
+ url = hostname if hostname.startswith("http") else f"https://{hostname}"
1802
+ store_tunnel_credentials(url, access_client_id, access_client_secret, env_file)
1803
+ # Record tunnel for scan command
1804
+ try:
1805
+ from synth_ai.cli.lib.tunnel_records import record_tunnel
1806
+ record_tunnel(
1807
+ url=url,
1808
+ port=cfg.port,
1809
+ mode="managed",
1810
+ pid=tunnel_proc.pid if tunnel_proc else None,
1811
+ hostname=hostname,
1812
+ local_host=cfg.host,
1813
+ task_app_path=str(cfg.task_app_path) if cfg.task_app_path else None,
1814
+ )
1815
+ except Exception:
1816
+ pass # Fail silently - records are optional
1817
+
1818
+ # If wait or keep_alive is True, block and keep processes alive until interrupted
1819
+ if wait or keep_alive:
1820
+ _keep_tunnel_alive(cfg.port, url)
1821
+ else:
1822
+ # Background mode: print URL and return immediately
1823
+ # Processes will keep running in background
1824
+ print(f"✓ Tunnel ready: {url}")
1825
+ print(f"⏳ Tunnel running in background (PID: {tunnel_proc.pid if tunnel_proc else 'N/A'})")
1826
+ print(" Press Ctrl+C in this process to stop, or use: pkill -f cloudflared")
1827
+
1828
+ return url
1829
+
1830
+ except Exception as exc:
1831
+ # Clean up tunnel process on error
1832
+ if tunnel_proc:
1833
+ stop_tunnel(tunnel_proc)
1834
+ _TUNNEL_PROCESSES.pop(cfg.port, None)
1835
+ # Remove record if it was created
1836
+ try:
1837
+ from synth_ai.cli.lib.tunnel_records import remove_tunnel_record
1838
+ remove_tunnel_record(cfg.port)
1839
+ except Exception:
1840
+ pass
1841
+ raise RuntimeError(f"Failed to deploy tunnel: {exc}") from exc
1842
+
1843
+
1844
+ def _keep_tunnel_alive(port: int, url: str) -> None:
1845
+ """
1846
+ Keep tunnel processes alive until interrupted.
1847
+
1848
+ This function blocks and monitors the tunnel process, similar to how
1849
+ local deployments block. Users can interrupt with Ctrl+C to stop.
1850
+
1851
+ Args:
1852
+ port: Port the tunnel is running on
1853
+ url: Public tunnel URL (for display)
1854
+ """
1855
+
1856
+ def signal_handler(signum, frame): # noqa: ARG001
1857
+ """Handle SIGINT/SIGTERM to cleanup gracefully."""
1858
+ if port in _TUNNEL_PROCESSES:
1859
+ stop_tunnel(_TUNNEL_PROCESSES[port])
1860
+ _TUNNEL_PROCESSES.pop(port, None)
1861
+ sys.exit(0)
1862
+
1863
+ # Register signal handlers for graceful shutdown
1864
+ signal.signal(signal.SIGINT, signal_handler)
1865
+ signal.signal(signal.SIGTERM, signal_handler)
1866
+
1867
+ print(f"✓ Tunnel ready: {url}")
1868
+ print("⏳ Keeping tunnel running... (Press Ctrl+C to stop)")
1869
+
1870
+ try:
1871
+ # Monitor tunnel process and keep alive
1872
+ while True:
1873
+ if port in _TUNNEL_PROCESSES:
1874
+ proc = _TUNNEL_PROCESSES[port]
1875
+ if isinstance(proc, subprocess.Popen) and proc.poll() is not None:
1876
+ print(f"❌ Tunnel process exited with code {proc.returncode}")
1877
+ break
1878
+ time.sleep(1)
1879
+ except KeyboardInterrupt:
1880
+ pass
1881
+ finally:
1882
+ # Cleanup on exit
1883
+ if port in _TUNNEL_PROCESSES:
1884
+ stop_tunnel(_TUNNEL_PROCESSES[port])
1885
+ _TUNNEL_PROCESSES.pop(port, None)
1886
+ print("\n🛑 Tunnel stopped")