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.
- synth_ai/__init__.py +19 -40
- synth_ai/__main__.py +30 -3
- synth_ai/cli/__init__.py +105 -70
- synth_ai/cli/__main__.py +42 -0
- synth_ai/cli/_internal/__init__.py +5 -0
- synth_ai/cli/_internal/modal_wrapper.py +31 -0
- synth_ai/cli/_internal/storage.py +20 -0
- synth_ai/cli/_internal/typer_patch.py +47 -0
- synth_ai/cli/_internal/validate_task_app.py +29 -0
- synth_ai/cli/agents/__init__.py +17 -0
- synth_ai/cli/agents/claude.py +77 -0
- synth_ai/cli/agents/codex.py +265 -0
- synth_ai/cli/agents/opencode.py +253 -0
- synth_ai/cli/commands/__init__.py +18 -0
- synth_ai/cli/commands/artifacts/__init__.py +13 -0
- synth_ai/cli/commands/artifacts/client.py +119 -0
- synth_ai/cli/commands/artifacts/config.py +57 -0
- synth_ai/cli/commands/artifacts/core.py +24 -0
- synth_ai/cli/commands/artifacts/download.py +188 -0
- synth_ai/cli/commands/artifacts/export.py +186 -0
- synth_ai/cli/commands/artifacts/list.py +156 -0
- synth_ai/cli/commands/artifacts/parsing.py +250 -0
- synth_ai/cli/commands/artifacts/show.py +336 -0
- synth_ai/cli/commands/baseline/__init__.py +12 -0
- synth_ai/cli/commands/baseline/core.py +636 -0
- synth_ai/cli/commands/baseline/list.py +94 -0
- synth_ai/cli/commands/demo/__init__.py +3 -0
- synth_ai/cli/commands/demo/core.py +153 -0
- synth_ai/cli/commands/eval/__init__.py +19 -0
- synth_ai/cli/commands/eval/core.py +1113 -0
- synth_ai/cli/commands/eval/errors.py +81 -0
- synth_ai/cli/commands/eval/validation.py +133 -0
- synth_ai/cli/commands/filter/__init__.py +12 -0
- synth_ai/cli/commands/filter/core.py +424 -0
- synth_ai/cli/commands/filter/errors.py +55 -0
- synth_ai/cli/commands/filter/validation.py +77 -0
- synth_ai/cli/commands/help/__init__.py +185 -0
- synth_ai/cli/commands/help/core.py +72 -0
- synth_ai/cli/commands/scan/__init__.py +19 -0
- synth_ai/cli/commands/scan/cloudflare_scanner.py +403 -0
- synth_ai/cli/commands/scan/core.py +344 -0
- synth_ai/cli/commands/scan/health_checker.py +242 -0
- synth_ai/cli/commands/scan/local_scanner.py +278 -0
- synth_ai/cli/commands/scan/models.py +83 -0
- synth_ai/cli/commands/smoke/__init__.py +7 -0
- synth_ai/cli/commands/smoke/core.py +1438 -0
- synth_ai/cli/commands/status/__init__.py +66 -0
- synth_ai/cli/commands/status/client.py +192 -0
- synth_ai/cli/commands/status/config.py +92 -0
- synth_ai/cli/commands/status/errors.py +20 -0
- synth_ai/cli/commands/status/formatters.py +164 -0
- synth_ai/cli/commands/status/subcommands/__init__.py +9 -0
- synth_ai/cli/commands/status/subcommands/files.py +79 -0
- synth_ai/cli/commands/status/subcommands/jobs.py +334 -0
- synth_ai/cli/commands/status/subcommands/models.py +79 -0
- synth_ai/cli/commands/status/subcommands/pricing.py +23 -0
- synth_ai/cli/commands/status/subcommands/runs.py +81 -0
- synth_ai/cli/commands/status/subcommands/session.py +182 -0
- synth_ai/cli/commands/status/subcommands/summary.py +47 -0
- synth_ai/cli/commands/status/subcommands/usage.py +203 -0
- synth_ai/cli/commands/status/utils.py +114 -0
- synth_ai/cli/commands/train/__init__.py +53 -0
- synth_ai/cli/commands/train/core.py +22 -0
- synth_ai/cli/commands/train/errors.py +117 -0
- synth_ai/cli/commands/train/judge_schemas.py +201 -0
- synth_ai/cli/commands/train/judge_validation.py +305 -0
- synth_ai/cli/commands/train/prompt_learning_validation.py +633 -0
- synth_ai/cli/commands/train/validation.py +392 -0
- synth_ai/cli/demo_apps/__init__.py +10 -0
- synth_ai/cli/demo_apps/core/__init__.py +28 -0
- synth_ai/cli/demo_apps/core/cli.py +1735 -0
- synth_ai/cli/demo_apps/crafter/crafter_fft_4b.toml +55 -0
- synth_ai/cli/demo_apps/crafter/grpo_crafter_task_app.py +186 -0
- synth_ai/cli/demo_apps/crafter/rl_from_base_qwen4b.toml +74 -0
- synth_ai/cli/demo_apps/demo_registry.py +176 -0
- synth_ai/cli/demo_apps/demo_task_apps/core.py +440 -0
- synth_ai/cli/demo_apps/demo_task_apps/crafter/__init__.py +1 -0
- synth_ai/cli/demo_apps/demo_task_apps/crafter/grpo_crafter_task_app.py +185 -0
- synth_ai/cli/demo_apps/demo_task_apps/math/modal_task_app.py +742 -0
- synth_ai/cli/demo_apps/demo_task_apps/math/task_app_entry.py +39 -0
- synth_ai/cli/demo_apps/math/__init__.py +1 -0
- synth_ai/cli/demo_apps/math/_common.py +16 -0
- synth_ai/cli/demo_apps/math/app.py +38 -0
- synth_ai/cli/demo_apps/math/config.toml +76 -0
- synth_ai/cli/demo_apps/math/deploy_modal.py +54 -0
- synth_ai/cli/demo_apps/math/modal_task_app.py +702 -0
- synth_ai/cli/demo_apps/math/task_app_entry.py +53 -0
- synth_ai/cli/demo_apps/mipro/main.py +271 -0
- synth_ai/cli/demo_apps/mipro/task_app.py +933 -0
- synth_ai/cli/demo_apps/mipro/train_cfg.toml +92 -0
- synth_ai/cli/demos/__init__.py +12 -0
- synth_ai/cli/demos/demo.py +32 -0
- synth_ai/cli/demos/rl_demo.py +254 -0
- synth_ai/cli/deploy.py +216 -0
- synth_ai/cli/infra/__init__.py +14 -0
- synth_ai/cli/infra/balance.py +216 -0
- synth_ai/cli/infra/mcp.py +35 -0
- synth_ai/cli/infra/modal_app.py +36 -0
- synth_ai/cli/infra/setup.py +69 -0
- synth_ai/cli/infra/status.py +16 -0
- synth_ai/cli/infra/turso.py +77 -0
- synth_ai/cli/lib/__init__.py +10 -0
- synth_ai/cli/lib/agents.py +76 -0
- synth_ai/cli/lib/apps/modal_app.py +101 -0
- synth_ai/cli/lib/apps/task_app.py +643 -0
- synth_ai/cli/lib/bin.py +39 -0
- synth_ai/cli/lib/env.py +375 -0
- synth_ai/cli/lib/errors.py +85 -0
- synth_ai/cli/lib/modal.py +315 -0
- synth_ai/cli/lib/plotting.py +126 -0
- synth_ai/cli/lib/prompt_args.py +39 -0
- synth_ai/cli/lib/prompts.py +284 -0
- synth_ai/cli/lib/sqld.py +122 -0
- synth_ai/cli/lib/task_app_discovery.py +884 -0
- synth_ai/cli/lib/task_app_env.py +295 -0
- synth_ai/cli/lib/train_cfgs.py +300 -0
- synth_ai/cli/lib/tunnel_records.py +207 -0
- synth_ai/cli/local/__init__.py +14 -0
- synth_ai/cli/local/experiment_queue/__init__.py +72 -0
- synth_ai/cli/local/experiment_queue/api_schemas.py +221 -0
- synth_ai/cli/local/experiment_queue/celery_app.py +208 -0
- synth_ai/cli/local/experiment_queue/config.py +128 -0
- synth_ai/cli/local/experiment_queue/config_utils.py +272 -0
- synth_ai/cli/local/experiment_queue/database.py +175 -0
- synth_ai/cli/local/experiment_queue/dispatcher.py +119 -0
- synth_ai/cli/local/experiment_queue/models.py +231 -0
- synth_ai/cli/local/experiment_queue/progress_info.py +160 -0
- synth_ai/cli/local/experiment_queue/results.py +373 -0
- synth_ai/cli/local/experiment_queue/schemas.py +131 -0
- synth_ai/cli/local/experiment_queue/service.py +344 -0
- synth_ai/cli/local/experiment_queue/status.py +372 -0
- synth_ai/cli/local/experiment_queue/status_tracker.py +360 -0
- synth_ai/cli/local/experiment_queue/tasks.py +1984 -0
- synth_ai/cli/local/experiment_queue/trace_storage.py +65 -0
- synth_ai/cli/local/experiment_queue/validation.py +157 -0
- synth_ai/cli/local/session/__init__.py +92 -0
- synth_ai/cli/local/session/client.py +383 -0
- synth_ai/cli/local/session/constants.py +63 -0
- synth_ai/cli/local/session/exceptions.py +105 -0
- synth_ai/cli/local/session/manager.py +139 -0
- synth_ai/cli/local/session/models.py +89 -0
- synth_ai/cli/local/session/query.py +110 -0
- synth_ai/cli/root.py +30 -6
- synth_ai/cli/task_apps/__init__.py +26 -0
- synth_ai/cli/task_apps/commands.py +3153 -0
- synth_ai/cli/task_apps/deploy.py +7 -0
- synth_ai/cli/task_apps/list.py +26 -0
- synth_ai/cli/task_apps/main.py +36 -0
- synth_ai/cli/task_apps/modal_serve.py +11 -0
- synth_ai/cli/task_apps/serve.py +11 -0
- synth_ai/cli/training/__init__.py +8 -0
- synth_ai/cli/training/train.py +5 -0
- synth_ai/cli/training/train_cfg.py +34 -0
- synth_ai/cli/training/watch.py +506 -0
- synth_ai/cli/turso.py +34 -55
- synth_ai/cli/usage.py +159 -0
- synth_ai/cli/utils/__init__.py +8 -0
- synth_ai/cli/utils/experiments.py +235 -0
- synth_ai/cli/utils/queue.py +504 -0
- synth_ai/cli/utils/recent.py +133 -0
- synth_ai/cli/utils/traces.py +164 -0
- synth_ai/contracts/__init__.py +67 -0
- synth_ai/core/__init__.py +100 -0
- synth_ai/core/_utils/__init__.py +54 -0
- synth_ai/core/_utils/base_url.py +10 -0
- synth_ai/core/_utils/http.py +10 -0
- synth_ai/core/_utils/prompts.py +14 -0
- synth_ai/core/_utils/task_app_state.py +12 -0
- synth_ai/core/_utils/user_config.py +10 -0
- synth_ai/core/apps/common.py +116 -0
- synth_ai/core/auth.py +95 -0
- synth_ai/core/cfgs.py +240 -0
- synth_ai/core/config/__init__.py +16 -0
- synth_ai/core/config/base.py +168 -0
- synth_ai/core/config/resolver.py +89 -0
- synth_ai/core/env.py +220 -0
- synth_ai/core/errors.py +126 -0
- synth_ai/core/http.py +230 -0
- synth_ai/core/integrations/__init__.py +11 -0
- synth_ai/core/integrations/cloudflare.py +1710 -0
- synth_ai/core/integrations/mcp/__init__.py +6 -0
- synth_ai/core/integrations/mcp/__main__.py +8 -0
- synth_ai/core/integrations/mcp/claude.py +36 -0
- synth_ai/core/integrations/mcp/main.py +254 -0
- synth_ai/core/integrations/mcp/setup.py +100 -0
- synth_ai/core/integrations/modal.py +277 -0
- synth_ai/core/json.py +72 -0
- synth_ai/core/log_filter.py +99 -0
- synth_ai/core/logging.py +82 -0
- synth_ai/core/paths.py +107 -0
- synth_ai/core/pricing.py +109 -0
- synth_ai/core/process.py +233 -0
- synth_ai/core/ssl.py +25 -0
- synth_ai/core/storage/__init__.py +71 -0
- synth_ai/core/task_app_state.py +318 -0
- synth_ai/core/telemetry.py +282 -0
- synth_ai/core/tracing_v3/__init__.py +99 -0
- synth_ai/core/tracing_v3/abstractions.py +302 -0
- synth_ai/core/tracing_v3/config.py +229 -0
- synth_ai/core/tracing_v3/constants.py +21 -0
- synth_ai/core/tracing_v3/db_config.py +182 -0
- synth_ai/core/tracing_v3/decorators.py +401 -0
- synth_ai/core/tracing_v3/llm_call_record_helpers.py +437 -0
- synth_ai/core/tracing_v3/migration_helper.py +119 -0
- synth_ai/core/tracing_v3/session_tracer.py +542 -0
- synth_ai/core/tracing_v3/storage/base.py +211 -0
- synth_ai/core/tracing_v3/storage/config.py +109 -0
- synth_ai/core/tracing_v3/storage/factory.py +39 -0
- synth_ai/core/tracing_v3/trace_utils.py +326 -0
- synth_ai/core/tracing_v3/turso/daemon.py +278 -0
- synth_ai/core/tracing_v3/turso/models.py +470 -0
- synth_ai/core/tracing_v3/turso/native_manager.py +1385 -0
- synth_ai/core/tracing_v3/utils.py +108 -0
- synth_ai/core/urls.py +18 -0
- synth_ai/core/user_config.py +137 -0
- synth_ai/core/uvicorn.py +222 -0
- synth_ai/data/__init__.py +110 -0
- synth_ai/data/enums.py +141 -0
- synth_ai/data/rewards.py +152 -0
- synth_ai/data/specs.py +36 -0
- synth_ai/data/traces.py +35 -0
- synth_ai/products/__init__.py +6 -0
- synth_ai/products/graph_evolve/__init__.py +46 -0
- synth_ai/products/graph_evolve/client.py +226 -0
- synth_ai/products/graph_evolve/config.py +591 -0
- synth_ai/products/graph_evolve/converters/__init__.py +42 -0
- synth_ai/products/graph_evolve/converters/openai_sft.py +484 -0
- synth_ai/products/graph_evolve/examples/hotpotqa/config.toml +109 -0
- synth_ai/products/graph_evolve/run.py +222 -0
- synth_ai/sdk/__init__.py +119 -0
- synth_ai/sdk/api/__init__.py +1 -0
- synth_ai/sdk/api/models/supported.py +514 -0
- synth_ai/sdk/api/research_agent/__init__.py +86 -0
- synth_ai/sdk/api/research_agent/cli.py +428 -0
- synth_ai/sdk/api/research_agent/config.py +357 -0
- synth_ai/sdk/api/research_agent/job.py +717 -0
- synth_ai/sdk/api/train/__init__.py +85 -0
- synth_ai/sdk/api/train/builders.py +895 -0
- synth_ai/sdk/api/train/cli.py +2188 -0
- synth_ai/sdk/api/train/config_finder.py +267 -0
- synth_ai/sdk/api/train/configs/__init__.py +65 -0
- synth_ai/sdk/api/train/configs/prompt_learning.py +1706 -0
- synth_ai/sdk/api/train/configs/rl.py +188 -0
- synth_ai/sdk/api/train/configs/sft.py +99 -0
- synth_ai/sdk/api/train/configs/shared.py +81 -0
- synth_ai/sdk/api/train/context_learning.py +312 -0
- synth_ai/sdk/api/train/env_resolver.py +418 -0
- synth_ai/sdk/api/train/graph_validators.py +216 -0
- synth_ai/sdk/api/train/graphgen.py +984 -0
- synth_ai/sdk/api/train/graphgen_models.py +823 -0
- synth_ai/sdk/api/train/graphgen_validators.py +109 -0
- synth_ai/sdk/api/train/pollers.py +124 -0
- synth_ai/sdk/api/train/progress/__init__.py +97 -0
- synth_ai/sdk/api/train/progress/dataclasses.py +569 -0
- synth_ai/sdk/api/train/progress/events.py +326 -0
- synth_ai/sdk/api/train/progress/results.py +428 -0
- synth_ai/sdk/api/train/progress/tracker.py +641 -0
- synth_ai/sdk/api/train/prompt_learning.py +470 -0
- synth_ai/sdk/api/train/rl.py +442 -0
- synth_ai/sdk/api/train/sft.py +396 -0
- synth_ai/sdk/api/train/summary.py +522 -0
- synth_ai/sdk/api/train/supported_algos.py +147 -0
- synth_ai/sdk/api/train/task_app.py +331 -0
- synth_ai/sdk/api/train/utils.py +279 -0
- synth_ai/sdk/api/train/validators.py +2424 -0
- synth_ai/sdk/baseline/__init__.py +25 -0
- synth_ai/sdk/baseline/config.py +209 -0
- synth_ai/sdk/baseline/discovery.py +216 -0
- synth_ai/sdk/baseline/execution.py +154 -0
- synth_ai/sdk/graphs/__init__.py +15 -0
- synth_ai/sdk/graphs/completions.py +570 -0
- synth_ai/sdk/inference/__init__.py +6 -0
- synth_ai/sdk/inference/client.py +128 -0
- synth_ai/sdk/jobs/__init__.py +16 -0
- synth_ai/sdk/jobs/client.py +371 -0
- synth_ai/sdk/judging/__init__.py +15 -0
- synth_ai/sdk/judging/base.py +24 -0
- synth_ai/sdk/judging/client.py +191 -0
- synth_ai/sdk/judging/schemas.py +222 -0
- synth_ai/sdk/learning/__init__.py +69 -0
- synth_ai/sdk/learning/client.py +240 -0
- synth_ai/sdk/learning/ft_client.py +7 -0
- synth_ai/sdk/learning/health.py +49 -0
- synth_ai/sdk/learning/jobs.py +202 -0
- synth_ai/sdk/learning/prompt_extraction.py +334 -0
- synth_ai/sdk/learning/prompt_learning_client.py +455 -0
- synth_ai/sdk/learning/prompt_learning_types.py +185 -0
- synth_ai/sdk/learning/rl/client.py +268 -0
- synth_ai/sdk/learning/rl/contracts.py +27 -0
- synth_ai/sdk/learning/rl/env_keys.py +166 -0
- synth_ai/sdk/learning/rl/secrets.py +13 -0
- synth_ai/sdk/learning/sft/client.py +95 -0
- synth_ai/sdk/learning/sft/config.py +270 -0
- synth_ai/sdk/learning/sft/data.py +698 -0
- synth_ai/sdk/learning/validators.py +52 -0
- synth_ai/sdk/research_agent/__init__.py +34 -0
- synth_ai/sdk/research_agent/container_builder.py +328 -0
- synth_ai/sdk/research_agent/container_spec.py +198 -0
- synth_ai/sdk/research_agent/defaults.py +34 -0
- synth_ai/sdk/research_agent/results_collector.py +69 -0
- synth_ai/sdk/specs/__init__.py +46 -0
- synth_ai/sdk/specs/dataclasses.py +149 -0
- synth_ai/sdk/specs/loader.py +144 -0
- synth_ai/sdk/specs/serializer.py +199 -0
- synth_ai/sdk/specs/validation.py +250 -0
- synth_ai/sdk/streaming/__init__.py +35 -0
- synth_ai/sdk/streaming/config.py +94 -0
- synth_ai/sdk/streaming/handlers.py +1997 -0
- synth_ai/sdk/streaming/streamer.py +704 -0
- synth_ai/sdk/streaming/types.py +112 -0
- synth_ai/sdk/task/__init__.py +151 -0
- synth_ai/sdk/task/apps/__init__.py +133 -0
- synth_ai/sdk/task/config.py +261 -0
- synth_ai/sdk/task/contracts.py +298 -0
- synth_ai/sdk/task/datasets.py +108 -0
- synth_ai/sdk/task/in_process.py +1190 -0
- synth_ai/sdk/task/in_process_runner.py +309 -0
- synth_ai/sdk/task/inference_api.py +299 -0
- synth_ai/sdk/task/proxy.py +287 -0
- synth_ai/sdk/task/rubrics/__init__.py +55 -0
- synth_ai/sdk/task/rubrics/loaders.py +156 -0
- synth_ai/sdk/task/rubrics.py +219 -0
- synth_ai/sdk/task/server.py +580 -0
- synth_ai/sdk/task/trace_correlation_helpers.py +506 -0
- synth_ai/sdk/task/tracing_utils.py +95 -0
- synth_ai/sdk/task/validators.py +456 -0
- synth_ai/sdk/tracing/__init__.py +39 -0
- synth_ai/sdk/training/__init__.py +102 -0
- synth_ai/sdk/usage/__init__.py +37 -0
- synth_ai/sdk/usage/client.py +171 -0
- synth_ai/sdk/usage/models.py +261 -0
- synth_ai/utils/__init__.py +213 -0
- synth_ai-0.4.1.dist-info/METADATA +195 -0
- synth_ai-0.4.1.dist-info/RECORD +379 -0
- synth_ai-0.4.1.dist-info/top_level.txt +1 -0
- examples/__init__.py +0 -16
- examples/analyze_semantic_words.sh +0 -17
- examples/crafter_debug_render.py +0 -186
- examples/dev/qwen3_32b_qlora_4xh100.toml +0 -40
- examples/multi_step/configs/README_verilog_rl.md +0 -77
- examples/multi_step/configs/VERILOG_REWARDS.md +0 -90
- examples/multi_step/configs/VERILOG_RL_CHECKLIST.md +0 -183
- examples/multi_step/configs/crafter_eval_synth_qwen4b.toml +0 -35
- examples/multi_step/configs/crafter_eval_text_only_groq_qwen32b.toml +0 -36
- examples/multi_step/configs/crafter_rl_outcome.toml +0 -74
- examples/multi_step/configs/crafter_rl_stepwise_hosted_judge.toml +0 -187
- examples/multi_step/configs/crafter_rl_stepwise_shaped.toml +0 -83
- examples/multi_step/configs/crafter_rl_stepwise_simple.toml +0 -78
- examples/multi_step/configs/crafter_synth_backend.md +0 -40
- examples/multi_step/configs/verilog_eval_groq_qwen32b.toml +0 -31
- examples/multi_step/configs/verilog_eval_synth_qwen8b.toml +0 -33
- examples/multi_step/configs/verilog_rl_lora.toml +0 -190
- examples/multi_step/crafter_rl_lora.md +0 -70
- examples/multi_step/judges/crafter_backend_judge.py +0 -220
- examples/multi_step/judges/verilog_backend_judge.py +0 -234
- examples/multi_step/readme.md +0 -48
- examples/multi_step/sse_metrics_streaming_notes.md +0 -357
- examples/multi_step/task_app_config_notes.md +0 -494
- examples/multi_step/verilog_rl_lora.md +0 -218
- examples/qwen_coder/README.md +0 -102
- examples/qwen_coder/_shared.py +0 -113
- examples/qwen_coder/configs/coder_lora_30b.toml +0 -61
- examples/qwen_coder/configs/coder_lora_4b.toml +0 -57
- examples/qwen_coder/configs/coder_lora_small.toml +0 -58
- examples/qwen_coder/generate_dataset.py +0 -98
- examples/qwen_coder/infer_ft_smoke.py +0 -65
- examples/qwen_coder/infer_prod_proxy.py +0 -73
- examples/qwen_coder/infer_via_synth.py +0 -87
- examples/qwen_coder/scripts/infer_coder.sh +0 -19
- examples/qwen_coder/scripts/train_coder_30b.sh +0 -22
- examples/qwen_coder/sft_full_17b.py +0 -103
- examples/qwen_coder/sft_lora_30b.py +0 -110
- examples/qwen_coder/subset_jsonl.py +0 -39
- examples/qwen_coder/todos.md +0 -38
- examples/qwen_coder/validate_jsonl.py +0 -60
- examples/rl/README.md +0 -169
- examples/rl/download_dataset.py +0 -80
- examples/run_crafter_demo.sh +0 -10
- examples/sft/README.md +0 -139
- examples/sft/configs/crafter_fft_qwen0p6b.toml +0 -44
- examples/sft/configs/crafter_lora_qwen0p6b.toml +0 -45
- examples/sft/evaluate.py +0 -119
- examples/sft/export_dataset.py +0 -117
- examples/sft/generate_traces.py +0 -164
- examples/swe/__init__.py +0 -12
- examples/swe/task_app/README.md +0 -105
- examples/swe/task_app/__init__.py +0 -2
- examples/swe/task_app/grpo_swe_mini.py +0 -601
- examples/swe/task_app/grpo_swe_mini_task_app.py +0 -136
- examples/swe/task_app/hosted/README.md +0 -173
- examples/swe/task_app/hosted/__init__.py +0 -5
- examples/swe/task_app/hosted/branching.py +0 -143
- examples/swe/task_app/hosted/environment_routes.py +0 -1289
- examples/swe/task_app/hosted/envs/__init__.py +0 -1
- examples/swe/task_app/hosted/envs/crafter/__init__.py +0 -6
- examples/swe/task_app/hosted/envs/crafter/app.py +0 -1
- examples/swe/task_app/hosted/envs/crafter/environment.py +0 -522
- examples/swe/task_app/hosted/envs/crafter/policy.py +0 -478
- examples/swe/task_app/hosted/envs/crafter/react_agent.py +0 -108
- examples/swe/task_app/hosted/envs/crafter/shared.py +0 -305
- examples/swe/task_app/hosted/envs/crafter/tools.py +0 -47
- examples/swe/task_app/hosted/envs/mini_swe/__init__.py +0 -8
- examples/swe/task_app/hosted/envs/mini_swe/environment.py +0 -1164
- examples/swe/task_app/hosted/envs/mini_swe/policy.py +0 -355
- examples/swe/task_app/hosted/envs/mini_swe/shared.py +0 -83
- examples/swe/task_app/hosted/envs/mini_swe/tools.py +0 -96
- examples/swe/task_app/hosted/hosted_app.py +0 -204
- examples/swe/task_app/hosted/inference/__init__.py +0 -5
- examples/swe/task_app/hosted/inference/openai_client.py +0 -618
- examples/swe/task_app/hosted/main.py +0 -100
- examples/swe/task_app/hosted/policy_routes.py +0 -1079
- examples/swe/task_app/hosted/registry.py +0 -195
- examples/swe/task_app/hosted/rollout.py +0 -1911
- examples/swe/task_app/hosted/storage/__init__.py +0 -5
- examples/swe/task_app/hosted/storage/volume.py +0 -211
- examples/swe/task_app/hosted/test_agents.py +0 -161
- examples/swe/task_app/hosted/test_service.py +0 -136
- examples/swe/task_app/hosted/utils.py +0 -62
- examples/task_apps/IMAGE_ONLY_EVAL_QUICKSTART.md +0 -258
- examples/task_apps/TESTING.md +0 -275
- examples/task_apps/crafter/CREATE_SFT_DATASET.md +0 -273
- examples/task_apps/crafter/EVAL_IMAGE_ONLY_RESULTS.md +0 -152
- examples/task_apps/crafter/FILTER_COMMAND_STATUS.md +0 -174
- examples/task_apps/crafter/FILTER_COMMAND_SUCCESS.md +0 -268
- examples/task_apps/crafter/QUERY_EXAMPLES.md +0 -203
- examples/task_apps/crafter/README_IMAGE_ONLY_EVAL.md +0 -316
- examples/task_apps/crafter/__init__.py +0 -0
- examples/task_apps/crafter/eval_image_only_gpt4o.toml +0 -28
- examples/task_apps/crafter/eval_text_only_groq_llama.toml +0 -36
- examples/task_apps/crafter/filter_sft_dataset.toml +0 -16
- examples/task_apps/crafter/task_app/README.md +0 -42
- examples/task_apps/crafter/task_app/__init__.py +0 -5
- examples/task_apps/crafter/task_app/grpo_crafter.py +0 -973
- examples/task_apps/crafter/task_app/grpo_crafter_task_app.py +0 -146
- examples/task_apps/crafter/task_app/synth_envs_hosted/README.md +0 -173
- examples/task_apps/crafter/task_app/synth_envs_hosted/__init__.py +0 -5
- examples/task_apps/crafter/task_app/synth_envs_hosted/branching.py +0 -143
- examples/task_apps/crafter/task_app/synth_envs_hosted/environment_routes.py +0 -1226
- examples/task_apps/crafter/task_app/synth_envs_hosted/envs/__init__.py +0 -1
- examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/__init__.py +0 -6
- examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/app.py +0 -1
- examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/environment.py +0 -532
- examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/policy.py +0 -547
- examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/react_agent.py +0 -123
- examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/shared.py +0 -305
- examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/tools.py +0 -47
- examples/task_apps/crafter/task_app/synth_envs_hosted/hosted_app.py +0 -204
- examples/task_apps/crafter/task_app/synth_envs_hosted/inference/__init__.py +0 -5
- examples/task_apps/crafter/task_app/synth_envs_hosted/inference/openai_client.py +0 -704
- examples/task_apps/crafter/task_app/synth_envs_hosted/main.py +0 -100
- examples/task_apps/crafter/task_app/synth_envs_hosted/policy_routes.py +0 -1152
- examples/task_apps/crafter/task_app/synth_envs_hosted/registry.py +0 -195
- examples/task_apps/crafter/task_app/synth_envs_hosted/rollout.py +0 -2160
- examples/task_apps/crafter/task_app/synth_envs_hosted/storage/__init__.py +0 -5
- examples/task_apps/crafter/task_app/synth_envs_hosted/storage/volume.py +0 -211
- examples/task_apps/crafter/task_app/synth_envs_hosted/test_agents.py +0 -161
- examples/task_apps/crafter/task_app/synth_envs_hosted/test_service.py +0 -136
- examples/task_apps/crafter/task_app/synth_envs_hosted/utils.py +0 -218
- examples/task_apps/dev/pokemon_emerald/__init__.py +0 -2
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/README.md +0 -811
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/__init__.py +0 -120
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/action.py +0 -160
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/memory.py +0 -155
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/perception.py +0 -69
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/planning.py +0 -96
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/simple.py +0 -1502
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/system_prompt.py +0 -4
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/grab_map.py +0 -68
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/manual.py +0 -216
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/__init__.py +0 -35
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/emerald_utils.py +0 -631
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/emulator.py +0 -1544
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/enums.py +0 -1428
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/memory_reader.py +0 -4848
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/types.py +0 -41
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/utils.py +0 -298
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pyproject.toml +0 -95
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/run.py +0 -204
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server/__init__.py +0 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server/app.py +0 -2152
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server/client.py +0 -429
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server/frame_server.py +0 -155
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/README.md +0 -78
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/__init__.py +0 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/run_tests.py +0 -122
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_agent_direct.py +0 -76
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_agent_prompts.py +0 -413
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_battle_state_formatting.py +0 -204
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_dialogue_detection.py +0 -133
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_dialogue_detection_comprehensive.py +0 -229
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_direct_agent_emulator.py +0 -300
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_fps_adjustment_pytest.py +0 -205
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_house_to_outside_direct.py +0 -200
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_house_to_outside_transition.py +0 -284
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_map_ground_truth_comparison.py +0 -468
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_memory_map.py +0 -575
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_server_map_validation.py +0 -311
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_torchic_state.py +0 -259
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/__init__.py +0 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/anticheat.py +0 -372
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/checkpoint.py +0 -296
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/error_handler.py +0 -275
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/get_local_ip.py +0 -22
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/helpers.py +0 -44
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/llm_logger.py +0 -514
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_formatter.py +0 -415
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_stitcher.py +0 -1763
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_stitcher_singleton.py +0 -33
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_trimmer.py +0 -106
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_visualizer.py +0 -334
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/ocr_dialogue.py +0 -1020
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/recording.py +0 -188
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/state_formatter.py +0 -1481
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/vlm.py +0 -862
- examples/task_apps/dev/pokemon_emerald/modal_app.py +0 -114
- examples/task_apps/dev/pokemon_emerald/task_app/README.md +0 -81
- examples/task_apps/dev/pokemon_emerald/task_app/__init__.py +0 -6
- examples/task_apps/dev/pokemon_emerald/task_app/pokemon_emerald.py +0 -685
- examples/task_apps/enron/__init__.py +0 -1
- examples/task_apps/enron/eval_groq_qwen32.toml +0 -16
- examples/task_apps/enron/filter_sft.toml +0 -5
- examples/task_apps/enron/task_app/README.md +0 -14
- examples/task_apps/enron/task_app/__init__.py +0 -1
- examples/task_apps/enron/task_app/grpo_enron.py +0 -906
- examples/task_apps/enron/task_app/grpo_enron_task_app.py +0 -146
- examples/task_apps/enron/tests/__init__.py +0 -4
- examples/task_apps/enron/tests/conftest.py +0 -115
- examples/task_apps/enron/tests/integration/__init__.py +0 -4
- examples/task_apps/enron/tests/integration/test_enron_eval.py +0 -179
- examples/task_apps/enron/tests/integration/test_enron_rollout.py +0 -135
- examples/task_apps/enron/tests/unit/__init__.py +0 -4
- examples/task_apps/enron/tests/unit/test_enron_environment.py +0 -126
- examples/task_apps/math/README.md +0 -22
- examples/task_apps/math/__init__.py +0 -0
- examples/task_apps/math/math_single_step.py +0 -1000
- examples/task_apps/math/math_task_app.py +0 -115
- examples/task_apps/pokemon_battle/__init__.py +0 -2
- examples/task_apps/pokemon_battle/modal_app.py +0 -104
- examples/task_apps/pokemon_battle/task_app/README.md +0 -68
- examples/task_apps/pokemon_battle/task_app/__init__.py +0 -6
- examples/task_apps/pokemon_battle/task_app/pokemon_showdown.py +0 -932
- examples/task_apps/pokemon_red/EVAL_IMAGE_ONLY_COMPLETE.md +0 -283
- examples/task_apps/pokemon_red/EVAL_IMAGE_ONLY_STATUS.md +0 -155
- examples/task_apps/pokemon_red/README.md +0 -357
- examples/task_apps/pokemon_red/README_IMAGE_ONLY_EVAL.md +0 -415
- examples/task_apps/pokemon_red/__init__.py +0 -3
- examples/task_apps/pokemon_red/eval_image_only_gpt4o.toml +0 -29
- examples/task_apps/pokemon_red/eval_pokemon_red_policy.py +0 -225
- examples/task_apps/pokemon_red/pallet_town_rl_config.toml +0 -75
- examples/task_apps/pokemon_red/task_app.py +0 -799
- examples/task_apps/pokemon_red/test_pallet_town_rewards.py +0 -193
- examples/task_apps/sokoban/README.md +0 -307
- examples/task_apps/sokoban/__init__.py +0 -3
- examples/task_apps/sokoban/eval_groq_qwen32.toml +0 -16
- examples/task_apps/sokoban/eval_openai_gpt5.toml +0 -16
- examples/task_apps/sokoban/filter_sft.toml +0 -5
- examples/task_apps/sokoban/task_app.py +0 -1058
- examples/task_apps/sokoban/tests/__init__.py +0 -4
- examples/task_apps/sokoban/tests/conftest.py +0 -113
- examples/task_apps/sokoban/tests/integration/__init__.py +0 -4
- examples/task_apps/sokoban/tests/integration/test_sokoban_eval.py +0 -57
- examples/task_apps/sokoban/tests/integration/test_sokoban_rollout.py +0 -198
- examples/task_apps/sokoban/tests/unit/__init__.py +0 -4
- examples/task_apps/sokoban/tests/unit/test_sokoban_environment.py +0 -114
- examples/task_apps/verilog/__init__.py +0 -1
- examples/task_apps/verilog/eval_groq_qwen32b.toml +0 -24
- examples/task_apps/verilog/filter_sft.toml +0 -5
- examples/task_apps/verilog/task_app/README.md +0 -12
- examples/task_apps/verilog/task_app/__init__.py +0 -1
- examples/task_apps/verilog/task_app/grpo_verilog.py +0 -1166
- examples/task_apps/verilog/task_app/grpo_verilog_task_app.py +0 -145
- examples/task_apps/verilog/tests/__init__.py +0 -4
- examples/task_apps/verilog/tests/conftest.py +0 -115
- examples/task_apps/verilog/tests/integration/__init__.py +0 -4
- examples/task_apps/verilog/tests/integration/test_verilog_eval.py +0 -181
- examples/task_apps/verilog/tests/integration/test_verilog_rollout.py +0 -55
- examples/task_apps/verilog/tests/unit/__init__.py +0 -4
- examples/task_apps/verilog/tests/unit/test_verilog_scoring.py +0 -118
- examples/vlm/PROPOSAL.md +0 -53
- examples/vlm/README.md +0 -68
- examples/vlm/configs/crafter_vlm_gpt4o.toml +0 -44
- examples/vlm/crafter_image_only_agent.py +0 -207
- examples/vlm/crafter_openai_vlm_agent.py +0 -277
- examples/vlm/filter_image_rows.py +0 -63
- examples/vlm/run_crafter_vlm_benchmark.py +0 -316
- examples/warming_up_to_rl/analyze_trace_db.py +0 -422
- examples/warming_up_to_rl/configs/crafter_fft.toml +0 -48
- examples/warming_up_to_rl/configs/crafter_fft_4b.toml +0 -54
- examples/warming_up_to_rl/configs/eval_fft_qwen4b.toml +0 -20
- examples/warming_up_to_rl/configs/eval_groq_qwen32b.toml +0 -13
- examples/warming_up_to_rl/configs/eval_modal_qwen4b.toml +0 -23
- examples/warming_up_to_rl/configs/eval_stepwise_complex.toml +0 -35
- examples/warming_up_to_rl/configs/eval_stepwise_consistent.toml +0 -26
- examples/warming_up_to_rl/configs/eval_stepwise_per_achievement.toml +0 -36
- examples/warming_up_to_rl/configs/eval_stepwise_simple.toml +0 -32
- examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +0 -83
- examples/warming_up_to_rl/configs/rl_from_ft.toml +0 -56
- examples/warming_up_to_rl/export_trace_sft.py +0 -723
- examples/warming_up_to_rl/groq_test.py +0 -97
- examples/warming_up_to_rl/manage_secrets.py +0 -131
- examples/warming_up_to_rl/old/event_rewards.md +0 -234
- examples/warming_up_to_rl/old/notes.md +0 -73
- examples/warming_up_to_rl/readme.md +0 -179
- examples/warming_up_to_rl/run_eval.py +0 -736
- examples/warming_up_to_rl/run_fft_and_save.py +0 -380
- examples/warming_up_to_rl/run_local_rollout.py +0 -239
- examples/warming_up_to_rl/run_local_rollout_modal.py +0 -248
- examples/warming_up_to_rl/run_local_rollout_parallel.py +0 -405
- examples/warming_up_to_rl/run_local_rollout_traced.py +0 -477
- examples/warming_up_to_rl/run_rl_and_save.py +0 -124
- examples/warming_up_to_rl/run_rollout_remote.py +0 -156
- examples/workflows/__init__.py +0 -0
- examples/workflows/math_rl/__init__.py +0 -0
- examples/workflows/math_rl/configs/eval_base_qwen.toml +0 -15
- examples/workflows/math_rl/configs/eval_rl_qwen.toml +0 -11
- examples/workflows/math_rl/configs/rl_from_base_qwen.toml +0 -35
- examples/workflows/math_rl/configs/rl_from_base_qwen17.toml +0 -74
- examples/workflows/math_rl/configs/rl_from_ft_qwen.toml +0 -35
- examples/workflows/math_rl/download_dataset.py +0 -80
- examples/workflows/math_rl/run_eval.py +0 -436
- examples/workflows/math_rl/run_rl_and_save.py +0 -111
- synth_ai/api/models/supported.py +0 -377
- synth_ai/api/train/__init__.py +0 -5
- synth_ai/api/train/builders.py +0 -351
- synth_ai/api/train/cli.py +0 -635
- synth_ai/api/train/config_finder.py +0 -228
- synth_ai/api/train/configs/__init__.py +0 -44
- synth_ai/api/train/configs/rl.py +0 -134
- synth_ai/api/train/configs/sft.py +0 -95
- synth_ai/api/train/configs/shared.py +0 -24
- synth_ai/api/train/env_resolver.py +0 -349
- synth_ai/api/train/pollers.py +0 -75
- synth_ai/api/train/supported_algos.py +0 -147
- synth_ai/api/train/task_app.py +0 -195
- synth_ai/api/train/utils.py +0 -225
- synth_ai/cli/_modal_wrapper.py +0 -29
- synth_ai/cli/_storage.py +0 -20
- synth_ai/cli/_typer_patch.py +0 -49
- synth_ai/cli/_validate_task_app.py +0 -11
- synth_ai/cli/balance.py +0 -216
- synth_ai/cli/calc.py +0 -84
- synth_ai/cli/demo.py +0 -165
- synth_ai/cli/legacy_root_backup.py +0 -468
- synth_ai/cli/man.py +0 -106
- synth_ai/cli/recent.py +0 -132
- synth_ai/cli/rl_demo.py +0 -254
- synth_ai/cli/status.py +0 -134
- synth_ai/cli/task_apps.py +0 -4523
- synth_ai/cli/traces.py +0 -164
- synth_ai/cli/tui.py +0 -57
- synth_ai/cli/watch.py +0 -506
- synth_ai/compound/cais.py +0 -0
- synth_ai/config/base_url.py +0 -107
- synth_ai/core/experiment.py +0 -13
- synth_ai/core/system.py +0 -15
- synth_ai/demo_registry.py +0 -295
- synth_ai/demos/core/__init__.py +0 -1
- synth_ai/demos/core/cli.py +0 -1718
- synth_ai/demos/demo_task_apps/core.py +0 -440
- synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +0 -184
- synth_ai/demos/demo_task_apps/math/deploy_task_app.sh +0 -22
- synth_ai/demos/demo_task_apps/math/modal_task_app.py +0 -739
- synth_ai/demos/demo_task_apps/math/task_app_entry.py +0 -37
- synth_ai/environments/__init__.py +0 -31
- synth_ai/environments/environment/__init__.py +0 -1
- synth_ai/environments/environment/artifacts/__init__.py +0 -1
- synth_ai/environments/environment/artifacts/base.py +0 -52
- synth_ai/environments/environment/core.py +0 -67
- synth_ai/environments/environment/db/__init__.py +0 -1
- synth_ai/environments/environment/db/sqlite.py +0 -45
- synth_ai/environments/environment/registry.py +0 -233
- synth_ai/environments/environment/resources/sqlite.py +0 -45
- synth_ai/environments/environment/results.py +0 -1
- synth_ai/environments/environment/rewards/__init__.py +0 -1
- synth_ai/environments/environment/rewards/core.py +0 -29
- synth_ai/environments/environment/shared_engine.py +0 -26
- synth_ai/environments/environment/tools/__init__.py +0 -200
- synth_ai/environments/examples/__init__.py +0 -1
- synth_ai/environments/examples/bandit/__init__.py +0 -33
- synth_ai/environments/examples/bandit/engine.py +0 -302
- synth_ai/environments/examples/bandit/environment.py +0 -194
- synth_ai/environments/examples/bandit/taskset.py +0 -200
- synth_ai/environments/examples/crafter_classic/__init__.py +0 -8
- synth_ai/environments/examples/crafter_classic/agent_demos/analyze_semantic_words_markdown.py +0 -250
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_comprehensive_evaluation.py +0 -59
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_evaluation_browser.py +0 -152
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_evaluation_config.toml +0 -24
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_evaluation_framework.py +0 -1194
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/crafter_synth_config.toml +0 -56
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/filter_config_modal.toml +0 -32
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/filter_traces_sft_turso.py +0 -738
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/kick_off_ft_modal.py +0 -384
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_action_results.py +0 -53
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_agent_actions.py +0 -178
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_latest_run.py +0 -222
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_lm_traces.py +0 -183
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_no_rewards.py +0 -210
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_trace_issue.py +0 -206
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/check_db_schema.py +0 -49
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/check_latest_results.py +0 -64
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/debug_agent_responses.py +0 -88
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/quick_trace_check.py +0 -77
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/compare_experiments.py +0 -324
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/filter_traces_sft_turso.py +0 -580
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/kick_off_ft_oai.py +0 -362
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/multi_model_config.toml +0 -49
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/analyze_enhanced_hooks.py +0 -332
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/analyze_hook_events.py +0 -97
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/analyze_hook_results.py +0 -217
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/check_hook_storage.py +0 -87
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/check_seeds.py +0 -88
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/compare_seed_performance.py +0 -195
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/custom_eval_pipelines.py +0 -400
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/plot_hook_frequency.py +0 -195
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/seed_analysis_summary.py +0 -56
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/run_rollouts_for_models_and_compare_v3.py +0 -858
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_quick_evaluation.py +0 -52
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_react_agent.py +0 -874
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_trace_evaluation.py +0 -1412
- synth_ai/environments/examples/crafter_classic/agent_demos/example_v3_usage.py +0 -216
- synth_ai/environments/examples/crafter_classic/agent_demos/old/compare_traces.py +0 -296
- synth_ai/environments/examples/crafter_classic/agent_demos/old/crafter_comprehensive_evaluation.py +0 -58
- synth_ai/environments/examples/crafter_classic/agent_demos/old/crafter_env_serialization.py +0 -464
- synth_ai/environments/examples/crafter_classic/agent_demos/old/crafter_evaluation_browser.py +0 -152
- synth_ai/environments/examples/crafter_classic/agent_demos/old/crafter_quick_evaluation.py +0 -51
- synth_ai/environments/examples/crafter_classic/agent_demos/old/crafter_trace_evaluation.py +0 -1412
- synth_ai/environments/examples/crafter_classic/agent_demos/old/debug_player_loss.py +0 -112
- synth_ai/environments/examples/crafter_classic/agent_demos/old/diagnose_service.py +0 -203
- synth_ai/environments/examples/crafter_classic/agent_demos/old/diagnose_slowness.py +0 -305
- synth_ai/environments/examples/crafter_classic/agent_demos/old/eval_by_difficulty.py +0 -126
- synth_ai/environments/examples/crafter_classic/agent_demos/old/eval_example.py +0 -94
- synth_ai/environments/examples/crafter_classic/agent_demos/old/explore_saved_states.py +0 -142
- synth_ai/environments/examples/crafter_classic/agent_demos/old/filter_traces_sft.py +0 -26
- synth_ai/environments/examples/crafter_classic/agent_demos/old/filter_traces_sft_OLD.py +0 -984
- synth_ai/environments/examples/crafter_classic/agent_demos/old/generate_ft_data_gemini.py +0 -724
- synth_ai/environments/examples/crafter_classic/agent_demos/old/generate_ft_data_modal.py +0 -386
- synth_ai/environments/examples/crafter_classic/agent_demos/old/generate_ft_metadata.py +0 -205
- synth_ai/environments/examples/crafter_classic/agent_demos/old/kick_off_ft_gemini.py +0 -150
- synth_ai/environments/examples/crafter_classic/agent_demos/old/kick_off_ft_modal.py +0 -283
- synth_ai/environments/examples/crafter_classic/agent_demos/old/prepare_vertex_ft.py +0 -280
- synth_ai/environments/examples/crafter_classic/agent_demos/old/profile_env_slowness.py +0 -456
- synth_ai/environments/examples/crafter_classic/agent_demos/old/replicate_issue.py +0 -166
- synth_ai/environments/examples/crafter_classic/agent_demos/old/run_and_eval.py +0 -102
- synth_ai/environments/examples/crafter_classic/agent_demos/old/run_comparison.py +0 -128
- synth_ai/environments/examples/crafter_classic/agent_demos/old/run_qwen_rollouts.py +0 -655
- synth_ai/environments/examples/crafter_classic/agent_demos/old/trace_eval_OLD.py +0 -202
- synth_ai/environments/examples/crafter_classic/agent_demos/old/validate_openai_format.py +0 -166
- synth_ai/environments/examples/crafter_classic/config_logging.py +0 -111
- synth_ai/environments/examples/crafter_classic/debug_translation.py +0 -0
- synth_ai/environments/examples/crafter_classic/engine.py +0 -579
- synth_ai/environments/examples/crafter_classic/engine_deterministic_patch.py +0 -64
- synth_ai/environments/examples/crafter_classic/engine_helpers/action_map.py +0 -6
- synth_ai/environments/examples/crafter_classic/engine_helpers/serialization.py +0 -75
- synth_ai/environments/examples/crafter_classic/engine_serialization_patch_v3.py +0 -267
- synth_ai/environments/examples/crafter_classic/environment.py +0 -495
- synth_ai/environments/examples/crafter_classic/taskset.py +0 -233
- synth_ai/environments/examples/crafter_classic/trace_hooks_v3.py +0 -228
- synth_ai/environments/examples/crafter_classic/world_config_patch_simple.py +0 -299
- synth_ai/environments/examples/crafter_custom/__init__.py +0 -4
- synth_ai/environments/examples/crafter_custom/agent_demos/__init__.py +0 -1
- synth_ai/environments/examples/crafter_custom/agent_demos/trace_eval.py +0 -202
- synth_ai/environments/examples/crafter_custom/crafter/__init__.py +0 -7
- synth_ai/environments/examples/crafter_custom/crafter/config.py +0 -182
- synth_ai/environments/examples/crafter_custom/crafter/constants.py +0 -8
- synth_ai/environments/examples/crafter_custom/crafter/engine.py +0 -269
- synth_ai/environments/examples/crafter_custom/crafter/env.py +0 -262
- synth_ai/environments/examples/crafter_custom/crafter/objects.py +0 -417
- synth_ai/environments/examples/crafter_custom/crafter/recorder.py +0 -187
- synth_ai/environments/examples/crafter_custom/crafter/worldgen.py +0 -118
- synth_ai/environments/examples/crafter_custom/dataset_builder.py +0 -373
- synth_ai/environments/examples/crafter_custom/environment.py +0 -312
- synth_ai/environments/examples/crafter_custom/old/analyze_diamond_issue.py +0 -159
- synth_ai/environments/examples/crafter_custom/old/analyze_diamond_spawning.py +0 -158
- synth_ai/environments/examples/crafter_custom/old/compare_worlds.py +0 -71
- synth_ai/environments/examples/crafter_custom/old/dataset_stats.py +0 -105
- synth_ai/environments/examples/crafter_custom/old/diamond_spawning_summary.py +0 -119
- synth_ai/environments/examples/crafter_custom/old/example_dataset_usage.py +0 -52
- synth_ai/environments/examples/crafter_custom/run_dataset.py +0 -305
- synth_ai/environments/examples/enron/art_helpers/email_search_tools.py +0 -156
- synth_ai/environments/examples/enron/art_helpers/local_email_db.py +0 -281
- synth_ai/environments/examples/enron/art_helpers/types_enron.py +0 -25
- synth_ai/environments/examples/enron/engine.py +0 -300
- synth_ai/environments/examples/enron/environment.py +0 -234
- synth_ai/environments/examples/enron/taskset.py +0 -112
- synth_ai/environments/examples/enron/units/keyword_stats.py +0 -112
- synth_ai/environments/examples/minigrid/__init__.py +0 -48
- synth_ai/environments/examples/minigrid/agent_demos/minigrid_evaluation_framework.py +0 -1188
- synth_ai/environments/examples/minigrid/agent_demos/minigrid_quick_evaluation.py +0 -48
- synth_ai/environments/examples/minigrid/agent_demos/minigrid_react_agent.py +0 -562
- synth_ai/environments/examples/minigrid/agent_demos/minigrid_trace_evaluation.py +0 -221
- synth_ai/environments/examples/minigrid/engine.py +0 -589
- synth_ai/environments/examples/minigrid/environment.py +0 -274
- synth_ai/environments/examples/minigrid/environment_mapping.py +0 -242
- synth_ai/environments/examples/minigrid/puzzle_loader.py +0 -417
- synth_ai/environments/examples/minigrid/taskset.py +0 -583
- synth_ai/environments/examples/nethack/__init__.py +0 -7
- synth_ai/environments/examples/nethack/achievements.py +0 -337
- synth_ai/environments/examples/nethack/agent_demos/nethack_evaluation_framework.py +0 -981
- synth_ai/environments/examples/nethack/agent_demos/nethack_quick_evaluation.py +0 -74
- synth_ai/environments/examples/nethack/agent_demos/nethack_react_agent.py +0 -831
- synth_ai/environments/examples/nethack/engine.py +0 -739
- synth_ai/environments/examples/nethack/environment.py +0 -256
- synth_ai/environments/examples/nethack/helpers/__init__.py +0 -41
- synth_ai/environments/examples/nethack/helpers/action_mapping.py +0 -301
- synth_ai/environments/examples/nethack/helpers/nle_wrapper.py +0 -402
- synth_ai/environments/examples/nethack/helpers/observation_utils.py +0 -433
- synth_ai/environments/examples/nethack/helpers/recording_wrapper.py +0 -200
- synth_ai/environments/examples/nethack/helpers/trajectory_recorder.py +0 -269
- synth_ai/environments/examples/nethack/helpers/visualization/replay_viewer.py +0 -308
- synth_ai/environments/examples/nethack/helpers/visualization/visualizer.py +0 -431
- synth_ai/environments/examples/nethack/taskset.py +0 -323
- synth_ai/environments/examples/red/__init__.py +0 -7
- synth_ai/environments/examples/red/agent_demos/__init__.py +0 -1
- synth_ai/environments/examples/red/config_logging.py +0 -110
- synth_ai/environments/examples/red/engine.py +0 -721
- synth_ai/environments/examples/red/engine_helpers/__init__.py +0 -1
- synth_ai/environments/examples/red/engine_helpers/memory_map.py +0 -35
- synth_ai/environments/examples/red/engine_helpers/reward_components.py +0 -276
- synth_ai/environments/examples/red/engine_helpers/reward_library/__init__.py +0 -142
- synth_ai/environments/examples/red/engine_helpers/reward_library/adaptive_rewards.py +0 -57
- synth_ai/environments/examples/red/engine_helpers/reward_library/battle_rewards.py +0 -284
- synth_ai/environments/examples/red/engine_helpers/reward_library/composite_rewards.py +0 -150
- synth_ai/environments/examples/red/engine_helpers/reward_library/economy_rewards.py +0 -138
- synth_ai/environments/examples/red/engine_helpers/reward_library/efficiency_rewards.py +0 -57
- synth_ai/environments/examples/red/engine_helpers/reward_library/exploration_rewards.py +0 -331
- synth_ai/environments/examples/red/engine_helpers/reward_library/novelty_rewards.py +0 -121
- synth_ai/environments/examples/red/engine_helpers/reward_library/pallet_town_progression.py +0 -477
- synth_ai/environments/examples/red/engine_helpers/reward_library/pallet_town_rewards.py +0 -559
- synth_ai/environments/examples/red/engine_helpers/reward_library/pokemon_rewards.py +0 -313
- synth_ai/environments/examples/red/engine_helpers/reward_library/social_rewards.py +0 -148
- synth_ai/environments/examples/red/engine_helpers/reward_library/story_rewards.py +0 -247
- synth_ai/environments/examples/red/engine_helpers/screen_analysis.py +0 -368
- synth_ai/environments/examples/red/engine_helpers/state_extraction.py +0 -172
- synth_ai/environments/examples/red/environment.py +0 -298
- synth_ai/environments/examples/red/taskset.py +0 -79
- synth_ai/environments/examples/red/units/__init__.py +0 -1
- synth_ai/environments/examples/sokoban/__init__.py +0 -1
- synth_ai/environments/examples/sokoban/agent_demos/sokoban_full_eval.py +0 -899
- synth_ai/environments/examples/sokoban/engine.py +0 -678
- synth_ai/environments/examples/sokoban/engine_helpers/__init__.py +0 -1
- synth_ai/environments/examples/sokoban/engine_helpers/room_utils.py +0 -657
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/__init__.py +0 -18
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/__init__.py +0 -3
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/boxoban_env.py +0 -131
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/render_utils.py +0 -370
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/room_utils.py +0 -332
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env.py +0 -306
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_fixed_targets.py +0 -67
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_pull.py +0 -115
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_two_player.py +0 -123
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_variations.py +0 -394
- synth_ai/environments/examples/sokoban/environment.py +0 -229
- synth_ai/environments/examples/sokoban/generate_verified_puzzles.py +0 -440
- synth_ai/environments/examples/sokoban/puzzle_loader.py +0 -312
- synth_ai/environments/examples/sokoban/taskset.py +0 -544
- synth_ai/environments/examples/tictactoe/__init__.py +0 -1
- synth_ai/environments/examples/tictactoe/engine.py +0 -368
- synth_ai/environments/examples/tictactoe/environment.py +0 -240
- synth_ai/environments/examples/tictactoe/taskset.py +0 -215
- synth_ai/environments/examples/verilog/__init__.py +0 -10
- synth_ai/environments/examples/verilog/engine.py +0 -421
- synth_ai/environments/examples/verilog/environment.py +0 -350
- synth_ai/environments/examples/verilog/taskset.py +0 -420
- synth_ai/environments/examples/wordle/__init__.py +0 -29
- synth_ai/environments/examples/wordle/engine.py +0 -398
- synth_ai/environments/examples/wordle/environment.py +0 -159
- synth_ai/environments/examples/wordle/helpers/generate_instances_wordfreq.py +0 -75
- synth_ai/environments/examples/wordle/taskset.py +0 -230
- synth_ai/environments/reproducibility/core.py +0 -42
- synth_ai/environments/reproducibility/helpers.py +0 -0
- synth_ai/environments/reproducibility/tree.py +0 -363
- synth_ai/environments/service/app.py +0 -97
- synth_ai/environments/service/core_routes.py +0 -1021
- synth_ai/environments/service/external_registry.py +0 -56
- synth_ai/environments/service/registry.py +0 -9
- synth_ai/environments/stateful/__init__.py +0 -1
- synth_ai/environments/stateful/core.py +0 -163
- synth_ai/environments/stateful/engine.py +0 -21
- synth_ai/environments/stateful/state.py +0 -7
- synth_ai/environments/tasks/api.py +0 -19
- synth_ai/environments/tasks/core.py +0 -81
- synth_ai/environments/tasks/filters.py +0 -40
- synth_ai/environments/tasks/utils.py +0 -90
- synth_ai/environments/v0_observability/history.py +0 -3
- synth_ai/environments/v0_observability/log.py +0 -2
- synth_ai/evals/__init__.py +0 -15
- synth_ai/evals/base.py +0 -13
- synth_ai/evals/client.py +0 -82
- synth_ai/handshake.py +0 -109
- synth_ai/http.py +0 -26
- synth_ai/http_client.py +0 -136
- synth_ai/inference/__init__.py +0 -5
- synth_ai/inference/client.py +0 -34
- synth_ai/jobs/client.py +0 -295
- synth_ai/judge_schemas.py +0 -127
- synth_ai/learning/__init__.py +0 -59
- synth_ai/learning/client.py +0 -241
- synth_ai/learning/ft_client.py +0 -7
- synth_ai/learning/health.py +0 -49
- synth_ai/learning/jobs.py +0 -201
- synth_ai/learning/rl/client.py +0 -267
- synth_ai/learning/rl/contracts.py +0 -27
- synth_ai/learning/rl/env_keys.py +0 -166
- synth_ai/learning/rl/secrets.py +0 -13
- synth_ai/learning/sft/client.py +0 -68
- synth_ai/learning/sft/config.py +0 -270
- synth_ai/learning/sft/data.py +0 -295
- synth_ai/learning/validators.py +0 -49
- synth_ai/lm/__init__.py +0 -25
- synth_ai/task/__init__.py +0 -121
- synth_ai/task/apps/__init__.py +0 -129
- synth_ai/task/config.py +0 -257
- synth_ai/task/contracts.py +0 -236
- synth_ai/task/datasets.py +0 -108
- synth_ai/task/proxy.py +0 -251
- synth_ai/task/rubrics/__init__.py +0 -56
- synth_ai/task/rubrics/loaders.py +0 -152
- synth_ai/task/server.py +0 -432
- synth_ai/task/trace_correlation_helpers.py +0 -315
- synth_ai/task/tracing_utils.py +0 -84
- synth_ai/task/validators.py +0 -418
- synth_ai/tracing_v3/__init__.py +0 -97
- synth_ai/tracing_v3/abstractions.py +0 -302
- synth_ai/tracing_v3/config.py +0 -84
- synth_ai/tracing_v3/db_config.py +0 -194
- synth_ai/tracing_v3/decorators.py +0 -398
- synth_ai/tracing_v3/llm_call_record_helpers.py +0 -391
- synth_ai/tracing_v3/migration_helper.py +0 -120
- synth_ai/tracing_v3/session_tracer.py +0 -540
- synth_ai/tracing_v3/storage/base.py +0 -210
- synth_ai/tracing_v3/storage/config.py +0 -75
- synth_ai/tracing_v3/storage/factory.py +0 -39
- synth_ai/tracing_v3/trace_utils.py +0 -317
- synth_ai/tracing_v3/turso/daemon.py +0 -151
- synth_ai/tracing_v3/turso/models.py +0 -469
- synth_ai/tracing_v3/turso/native_manager.py +0 -1209
- synth_ai/tracing_v3/utils.py +0 -108
- synth_ai/tui/__init__.py +0 -5
- synth_ai/tui/__main__.py +0 -13
- synth_ai/tui/cli/__init__.py +0 -1
- synth_ai/tui/cli/query_experiments.py +0 -164
- synth_ai/tui/cli/query_experiments_v3.py +0 -164
- synth_ai/tui/dashboard.py +0 -906
- synth_ai/v0/api/__init__.py +0 -8
- synth_ai/v0/api/models/__init__.py +0 -8
- synth_ai/v0/api/models/supported.py +0 -8
- synth_ai/v0/config/__init__.py +0 -15
- synth_ai/v0/config/base_url.py +0 -12
- synth_ai/v0/lm/__init__.py +0 -51
- synth_ai/v0/lm/caching/__init__.py +0 -0
- synth_ai/v0/lm/caching/constants.py +0 -6
- synth_ai/v0/lm/caching/dbs.py +0 -0
- synth_ai/v0/lm/caching/ephemeral.py +0 -100
- synth_ai/v0/lm/caching/handler.py +0 -137
- synth_ai/v0/lm/caching/initialize.py +0 -11
- synth_ai/v0/lm/caching/persistent.py +0 -114
- synth_ai/v0/lm/config.py +0 -115
- synth_ai/v0/lm/constants.py +0 -32
- synth_ai/v0/lm/core/__init__.py +0 -8
- synth_ai/v0/lm/core/all.py +0 -73
- synth_ai/v0/lm/core/exceptions.py +0 -5
- synth_ai/v0/lm/core/main.py +0 -331
- synth_ai/v0/lm/core/main_v3.py +0 -594
- synth_ai/v0/lm/core/synth_models.py +0 -35
- synth_ai/v0/lm/core/vendor_clients.py +0 -190
- synth_ai/v0/lm/cost/__init__.py +0 -0
- synth_ai/v0/lm/cost/monitor.py +0 -1
- synth_ai/v0/lm/cost/statefulness.py +0 -1
- synth_ai/v0/lm/injection.py +0 -80
- synth_ai/v0/lm/overrides.py +0 -206
- synth_ai/v0/lm/provider_support/__init__.py +0 -8
- synth_ai/v0/lm/provider_support/anthropic.py +0 -972
- synth_ai/v0/lm/provider_support/openai.py +0 -1139
- synth_ai/v0/lm/provider_support/suppress_logging.py +0 -31
- synth_ai/v0/lm/structured_outputs/__init__.py +0 -0
- synth_ai/v0/lm/structured_outputs/handler.py +0 -440
- synth_ai/v0/lm/structured_outputs/inject.py +0 -297
- synth_ai/v0/lm/structured_outputs/rehabilitate.py +0 -185
- synth_ai/v0/lm/tools/__init__.py +0 -3
- synth_ai/v0/lm/tools/base.py +0 -172
- synth_ai/v0/lm/unified_interface.py +0 -202
- synth_ai/v0/lm/vendors/__init__.py +0 -0
- synth_ai/v0/lm/vendors/base.py +0 -81
- synth_ai/v0/lm/vendors/core/__init__.py +0 -0
- synth_ai/v0/lm/vendors/core/anthropic_api.py +0 -387
- synth_ai/v0/lm/vendors/core/gemini_api.py +0 -292
- synth_ai/v0/lm/vendors/core/mistral_api.py +0 -322
- synth_ai/v0/lm/vendors/core/openai_api.py +0 -227
- synth_ai/v0/lm/vendors/core/synth_dev_api.py +0 -0
- synth_ai/v0/lm/vendors/local/__init__.py +0 -0
- synth_ai/v0/lm/vendors/local/ollama.py +0 -0
- synth_ai/v0/lm/vendors/openai_standard.py +0 -782
- synth_ai/v0/lm/vendors/openai_standard_responses.py +0 -259
- synth_ai/v0/lm/vendors/retries.py +0 -22
- synth_ai/v0/lm/vendors/supported/__init__.py +0 -0
- synth_ai/v0/lm/vendors/supported/custom_endpoint.py +0 -415
- synth_ai/v0/lm/vendors/supported/deepseek.py +0 -69
- synth_ai/v0/lm/vendors/supported/grok.py +0 -75
- synth_ai/v0/lm/vendors/supported/groq.py +0 -16
- synth_ai/v0/lm/vendors/supported/ollama.py +0 -15
- synth_ai/v0/lm/vendors/supported/openrouter.py +0 -74
- synth_ai/v0/lm/vendors/supported/together.py +0 -11
- synth_ai/v0/lm/vendors/synth_client.py +0 -835
- synth_ai/v0/lm/warmup.py +0 -186
- synth_ai/v0/tracing/__init__.py +0 -0
- synth_ai/v0/tracing/abstractions.py +0 -224
- synth_ai/v0/tracing/base_client.py +0 -91
- synth_ai/v0/tracing/client_manager.py +0 -131
- synth_ai/v0/tracing/config.py +0 -142
- synth_ai/v0/tracing/context.py +0 -146
- synth_ai/v0/tracing/decorators.py +0 -682
- synth_ai/v0/tracing/events/__init__.py +0 -0
- synth_ai/v0/tracing/events/manage.py +0 -147
- synth_ai/v0/tracing/events/scope.py +0 -86
- synth_ai/v0/tracing/events/store.py +0 -228
- synth_ai/v0/tracing/immediate_client.py +0 -151
- synth_ai/v0/tracing/local.py +0 -18
- synth_ai/v0/tracing/log_client_base.py +0 -73
- synth_ai/v0/tracing/retry_queue.py +0 -186
- synth_ai/v0/tracing/trackers.py +0 -515
- synth_ai/v0/tracing/upload.py +0 -409
- synth_ai/v0/tracing/utils.py +0 -9
- synth_ai/v0/tracing_v1/__init__.py +0 -16
- synth_ai/v0/tracing_v1/abstractions.py +0 -224
- synth_ai/v0/tracing_v1/base_client.py +0 -91
- synth_ai/v0/tracing_v1/client_manager.py +0 -131
- synth_ai/v0/tracing_v1/config.py +0 -142
- synth_ai/v0/tracing_v1/context.py +0 -146
- synth_ai/v0/tracing_v1/decorators.py +0 -703
- synth_ai/v0/tracing_v1/events/__init__.py +0 -0
- synth_ai/v0/tracing_v1/events/manage.py +0 -147
- synth_ai/v0/tracing_v1/events/scope.py +0 -86
- synth_ai/v0/tracing_v1/events/store.py +0 -228
- synth_ai/v0/tracing_v1/immediate_client.py +0 -151
- synth_ai/v0/tracing_v1/local.py +0 -18
- synth_ai/v0/tracing_v1/log_client_base.py +0 -73
- synth_ai/v0/tracing_v1/retry_queue.py +0 -186
- synth_ai/v0/tracing_v1/trackers.py +0 -515
- synth_ai/v0/tracing_v1/upload.py +0 -527
- synth_ai/v0/tracing_v1/utils.py +0 -9
- synth_ai/v0/tracing_v3/__init__.py +0 -10
- synth_ai/v0/tracing_v3/abstractions.py +0 -3
- synth_ai/v0/tracing_v3/decorators.py +0 -3
- synth_ai/v0/tracing_v3/llm_call_record_helpers.py +0 -3
- synth_ai/v0/tracing_v3/session_tracer.py +0 -3
- synth_ai-0.2.14.dist-info/METADATA +0 -139
- synth_ai-0.2.14.dist-info/RECORD +0 -762
- synth_ai-0.2.14.dist-info/top_level.txt +0 -2
- /synth_ai/{demos/demo_task_apps → cli/demo_apps}/crafter/__init__.py +0 -0
- /synth_ai/{demos → cli/demo_apps}/demo_task_apps/__init__.py +0 -0
- /synth_ai/{demos → cli/demo_apps}/demo_task_apps/crafter/configs/crafter_fft_4b.toml +0 -0
- /synth_ai/{demos → cli/demo_apps}/demo_task_apps/crafter/configs/rl_from_base_qwen4b.toml +0 -0
- /synth_ai/{demos → cli/demo_apps}/demo_task_apps/math/__init__.py +0 -0
- /synth_ai/{demos → cli/demo_apps}/demo_task_apps/math/_common.py +0 -0
- /synth_ai/{demos → cli/demo_apps}/demo_task_apps/math/app.py +0 -0
- /synth_ai/{demos → cli/demo_apps}/demo_task_apps/math/config.toml +0 -0
- /synth_ai/{demos → cli/demo_apps}/demo_task_apps/math/deploy_modal.py +0 -0
- {examples/task_apps → synth_ai/core/apps}/__init__.py +0 -0
- /synth_ai/{tracing_v3 → core/tracing_v3}/examples/basic_usage.py +0 -0
- /synth_ai/{tracing_v3 → core/tracing_v3}/hooks.py +0 -0
- /synth_ai/{tracing_v3 → core/tracing_v3}/lm_call_record_abstractions.py +0 -0
- /synth_ai/{tracing_v3 → core/tracing_v3}/replica_sync.py +0 -0
- /synth_ai/{tracing_v3 → core/tracing_v3}/serialization.py +0 -0
- /synth_ai/{tracing_v3 → core/tracing_v3}/storage/__init__.py +0 -0
- /synth_ai/{tracing_v3 → core/tracing_v3}/storage/exceptions.py +0 -0
- /synth_ai/{tracing_v3 → core/tracing_v3}/storage/types.py +0 -0
- /synth_ai/{tracing_v3 → core/tracing_v3}/storage/utils.py +0 -0
- /synth_ai/{tracing_v3 → core/tracing_v3}/turso/__init__.py +0 -0
- /synth_ai/{evals → sdk/judging}/types.py +0 -0
- /synth_ai/{learning → sdk/learning}/algorithms.py +0 -0
- /synth_ai/{learning → sdk/learning}/config.py +0 -0
- /synth_ai/{learning → sdk/learning}/constants.py +0 -0
- /synth_ai/{learning → sdk/learning}/core.py +0 -0
- /synth_ai/{learning → sdk/learning}/gateway.py +0 -0
- /synth_ai/{learning → sdk/learning}/rl/__init__.py +0 -0
- /synth_ai/{learning → sdk/learning}/rl/config.py +0 -0
- /synth_ai/{learning → sdk/learning}/rl_client.py +0 -0
- /synth_ai/{learning → sdk/learning}/sft/__init__.py +0 -0
- /synth_ai/{learning → sdk/learning}/sse.py +0 -0
- /synth_ai/{task → sdk/task}/auth.py +0 -0
- /synth_ai/{task → sdk/task}/client.py +0 -0
- /synth_ai/{task → sdk/task}/errors.py +0 -0
- /synth_ai/{task → sdk/task}/health.py +0 -0
- /synth_ai/{task → sdk/task}/json.py +0 -0
- /synth_ai/{task → sdk/task}/rubrics/models.py +0 -0
- /synth_ai/{task → sdk/task}/rubrics/scoring.py +0 -0
- /synth_ai/{task → sdk/task}/rubrics/strict.py +0 -0
- /synth_ai/{task → sdk/task}/vendors.py +0 -0
- {synth_ai-0.2.14.dist-info → synth_ai-0.4.1.dist-info}/WHEEL +0 -0
- {synth_ai-0.2.14.dist-info → synth_ai-0.4.1.dist-info}/entry_points.txt +0 -0
- {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")
|