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