coderouter-cli 2.12.0__tar.gz → 2.13.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (301) hide show
  1. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/CHANGELOG.md +91 -0
  2. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/PKG-INFO +1 -1
  3. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/cli.py +20 -21
  4. coderouter_cli-2.13.0/coderouter/config/loader.py +264 -0
  5. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/config/schemas.py +93 -47
  6. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/guards/self_healing.py +58 -3
  7. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/ingress/app.py +72 -29
  8. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/ingress/launcher_routes.py +237 -61
  9. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/launcher_devices.py +11 -4
  10. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/pyproject.toml +1 -1
  11. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/conftest.py +1 -0
  12. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_cli.py +68 -0
  13. coderouter_cli-2.13.0/tests/test_config_cwd_warning.py +477 -0
  14. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_fix_h8_launcher_auth.py +5 -4
  15. coderouter_cli-2.13.0/tests/test_fix_v213_launcher_hardening.py +604 -0
  16. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_launcher_devices.py +14 -0
  17. coderouter_cli-2.13.0/tests/test_launcher_gui_sweep_close.py +444 -0
  18. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_launcher_variant_sweep.py +8 -2
  19. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_self_healing.py +157 -2
  20. coderouter_cli-2.12.0/coderouter/config/loader.py +0 -165
  21. coderouter_cli-2.12.0/tests/test_config_cwd_warning.py +0 -268
  22. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/.gitignore +0 -0
  23. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/LICENSE +0 -0
  24. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/README.en.md +0 -0
  25. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/README.md +0 -0
  26. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/__init__.py +0 -0
  27. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/__main__.py +0 -0
  28. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/adapters/__init__.py +0 -0
  29. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/adapters/anthropic_native.py +0 -0
  30. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/adapters/base.py +0 -0
  31. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/adapters/openai_compat.py +0 -0
  32. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/adapters/registry.py +0 -0
  33. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/cli_stats.py +0 -0
  34. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/config/__init__.py +0 -0
  35. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/config/capability_registry.py +0 -0
  36. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/config/env_file.py +0 -0
  37. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/cost.py +0 -0
  38. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/data/__init__.py +0 -0
  39. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/data/model-capabilities.yaml +0 -0
  40. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/doctor.py +0 -0
  41. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/doctor_apply.py +0 -0
  42. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/env_security.py +0 -0
  43. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/errors.py +0 -0
  44. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/gguf_introspect.py +0 -0
  45. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/guards/__init__.py +0 -0
  46. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/guards/_fingerprint.py +0 -0
  47. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/guards/backend_health.py +0 -0
  48. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/guards/context_budget.py +0 -0
  49. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/guards/continuous_probe.py +0 -0
  50. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/guards/drift_actions.py +0 -0
  51. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/guards/drift_detection.py +0 -0
  52. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/guards/memory_budget.py +0 -0
  53. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/guards/memory_pressure.py +0 -0
  54. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/guards/tool_loop.py +0 -0
  55. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/hardware.py +0 -0
  56. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/ingress/__init__.py +0 -0
  57. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/ingress/anthropic_routes.py +0 -0
  58. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/ingress/dashboard_routes.py +0 -0
  59. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/ingress/metrics_routes.py +0 -0
  60. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/ingress/openai_routes.py +0 -0
  61. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/language_tax.py +0 -0
  62. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/launcher_speculative.py +0 -0
  63. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/launcher_swap.py +0 -0
  64. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/logging.py +0 -0
  65. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/metrics/__init__.py +0 -0
  66. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/metrics/collector.py +0 -0
  67. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/metrics/prometheus.py +0 -0
  68. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/output_filters.py +0 -0
  69. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/plugins/__init__.py +0 -0
  70. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/plugins/base.py +0 -0
  71. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/plugins/loader.py +0 -0
  72. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/plugins/registry.py +0 -0
  73. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/py.typed +0 -0
  74. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/routing/__init__.py +0 -0
  75. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/routing/adaptive.py +0 -0
  76. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/routing/auto_router.py +0 -0
  77. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/routing/budget.py +0 -0
  78. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/routing/capability.py +0 -0
  79. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/routing/fallback.py +0 -0
  80. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/state/__init__.py +0 -0
  81. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/state/audit_log.py +0 -0
  82. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/state/replay.py +0 -0
  83. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/state/request_log.py +0 -0
  84. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/state/store.py +0 -0
  85. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/state/suggest_rules.py +0 -0
  86. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/token_estimation.py +0 -0
  87. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/token_estimation_accurate.py +0 -0
  88. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/translation/__init__.py +0 -0
  89. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/translation/anthropic.py +0 -0
  90. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/translation/convert.py +0 -0
  91. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/translation/tool_repair.py +0 -0
  92. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/coderouter/vscode_init.py +0 -0
  93. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/README.en.md +0 -0
  94. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/README.md +0 -0
  95. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/assets/dashboard-demo.png +0 -0
  96. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/backends/claude-code-llamacpp-vllm.en.md +0 -0
  97. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/backends/claude-code-llamacpp-vllm.md +0 -0
  98. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/backends/external-agents.en.md +0 -0
  99. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/backends/external-agents.md +0 -0
  100. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/backends/gguf_dl.en.md +0 -0
  101. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/backends/gguf_dl.md +0 -0
  102. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/backends/hf-ollama-models.en.md +0 -0
  103. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/backends/hf-ollama-models.md +0 -0
  104. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/backends/install-backends.en.md +0 -0
  105. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/backends/install-backends.md +0 -0
  106. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/backends/launcher-quickstart.en.md +0 -0
  107. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/backends/launcher-quickstart.md +0 -0
  108. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/backends/launcher.en.md +0 -0
  109. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/backends/launcher.md +0 -0
  110. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/backends/llamacpp-direct.en.md +0 -0
  111. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/backends/llamacpp-direct.md +0 -0
  112. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/backends/lmstudio-direct.en.md +0 -0
  113. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/backends/lmstudio-direct.md +0 -0
  114. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/backends/verify-ollama-0.23.1.md +0 -0
  115. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/concepts/architecture.en.md +0 -0
  116. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/concepts/architecture.md +0 -0
  117. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/concepts/context-budget.en.md +0 -0
  118. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/concepts/context-budget.md +0 -0
  119. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/concepts/continuous-probing.en.md +0 -0
  120. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/concepts/continuous-probing.md +0 -0
  121. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/concepts/drift-detection.en.md +0 -0
  122. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/concepts/drift-detection.md +0 -0
  123. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/concepts/partial-stitch.en.md +0 -0
  124. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/concepts/partial-stitch.md +0 -0
  125. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/designs/agent-cli-plugin-extraction.md +0 -0
  126. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/designs/external-agents-adapter.md +0 -0
  127. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/designs/launcher-model-swap.md +0 -0
  128. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/designs/launcher-multi-build.md +0 -0
  129. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/designs/orchestration-companion.md +0 -0
  130. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/designs/v1.5-dashboard-mockup.html +0 -0
  131. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/designs/v1.6-auto-router-verification.md +0 -0
  132. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/designs/v1.6-auto-router.md +0 -0
  133. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/guides/free-tier-guide.en.md +0 -0
  134. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/guides/free-tier-guide.md +0 -0
  135. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/guides/language-tax.en.md +0 -0
  136. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/guides/language-tax.md +0 -0
  137. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/guides/remote-access.en.md +0 -0
  138. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/guides/remote-access.md +0 -0
  139. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/guides/security.en.md +0 -0
  140. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/guides/security.md +0 -0
  141. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/guides/subagent-routing.en.md +0 -0
  142. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/guides/subagent-routing.md +0 -0
  143. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/guides/troubleshooting.en.md +0 -0
  144. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/guides/troubleshooting.md +0 -0
  145. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/guides/usage-guide.en.md +0 -0
  146. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/guides/usage-guide.md +0 -0
  147. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/guides/vscode.md +0 -0
  148. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/low-memory-integration.en.md +0 -0
  149. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/low-memory-integration.md +0 -0
  150. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/openrouter-roster/CHANGES.md +0 -0
  151. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/openrouter-roster/README.md +0 -0
  152. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/openrouter-roster/latest.json +0 -0
  153. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/retrospectives/v0.4.md +0 -0
  154. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/retrospectives/v0.5-verify.md +0 -0
  155. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/retrospectives/v0.5.md +0 -0
  156. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/retrospectives/v0.6.md +0 -0
  157. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/retrospectives/v0.7.md +0 -0
  158. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/retrospectives/v1.0-verify.md +0 -0
  159. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/retrospectives/v1.0.md +0 -0
  160. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/start/quickstart.en.md +0 -0
  161. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/start/quickstart.md +0 -0
  162. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/start/when-do-i-need-coderouter.en.md +0 -0
  163. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/docs/start/when-do-i-need-coderouter.md +0 -0
  164. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/examples/.env.example +0 -0
  165. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/examples/README.md +0 -0
  166. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/examples/providers-agent-cli.yaml +0 -0
  167. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/examples/providers-multiagent.yaml +0 -0
  168. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/examples/providers.context-budget-test.yaml +0 -0
  169. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/examples/providers.llamacpp-vllm.yaml +0 -0
  170. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/examples/providers.nvidia-nim.yaml +0 -0
  171. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/examples/providers.ollama-auto-custom.yaml +0 -0
  172. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/examples/providers.ollama-auto.yaml +0 -0
  173. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/examples/providers.ollama-free-chain.yaml +0 -0
  174. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/examples/providers.raspberrypi.yaml +0 -0
  175. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/examples/providers.swap.yaml +0 -0
  176. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/examples/providers.yaml +0 -0
  177. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/scripts/demo_traffic.sh +0 -0
  178. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/scripts/openrouter_roster_diff.py +0 -0
  179. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/scripts/smoke_v2_2.sh +0 -0
  180. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/scripts/verify-providers.yaml +0 -0
  181. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/scripts/verify_ollama_0_23.py +0 -0
  182. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/scripts/verify_v0_5.sh +0 -0
  183. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/scripts/verify_v1_0.sh +0 -0
  184. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/__init__.py +0 -0
  185. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_adapter_anthropic.py +0 -0
  186. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_agent_cli.py +0 -0
  187. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_audit_log.py +0 -0
  188. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_auto_router.py +0 -0
  189. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_auto_router_cjk.py +0 -0
  190. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_backend_health.py +0 -0
  191. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_budget.py +0 -0
  192. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_capability.py +0 -0
  193. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_capability_degraded_payload.py +0 -0
  194. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_capability_registry.py +0 -0
  195. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_capability_registry_cache_control.py +0 -0
  196. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_capability_tool_choice.py +0 -0
  197. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_claude_code_suitability.py +0 -0
  198. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_cli_bind_warning.py +0 -0
  199. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_cli_stats.py +0 -0
  200. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_config.py +0 -0
  201. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_config_shim_actions.py +0 -0
  202. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_context_budget.py +0 -0
  203. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_continuous_probe.py +0 -0
  204. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_dashboard_endpoint.py +0 -0
  205. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_doctor.py +0 -0
  206. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_doctor_apply.py +0 -0
  207. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_doctor_cache_probe.py +0 -0
  208. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_drift_actions.py +0 -0
  209. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_drift_detection.py +0 -0
  210. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_drift_detection_integration.py +0 -0
  211. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_env_file.py +0 -0
  212. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_env_security.py +0 -0
  213. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_errors.py +0 -0
  214. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_examples_yaml.py +0 -0
  215. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_examples_yaml_roundtrip.py +0 -0
  216. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_fallback.py +0 -0
  217. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_fallback_anthropic.py +0 -0
  218. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_fallback_cache_control.py +0 -0
  219. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_fallback_cache_observed.py +0 -0
  220. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_fallback_cache_strip.py +0 -0
  221. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_fallback_empty_response.py +0 -0
  222. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_fallback_misconfig_warn.py +0 -0
  223. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_fallback_paid_gate.py +0 -0
  224. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_fallback_thinking.py +0 -0
  225. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_fallback_tool_choice_emulate.py +0 -0
  226. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_fix_h1_prometheus_drift.py +0 -0
  227. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_fix_h2_h6_translation.py +0 -0
  228. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_fix_h3_client_reuse.py +0 -0
  229. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_fix_h4_h5_guards.py +0 -0
  230. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_fix_h7_probe_restore.py +0 -0
  231. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_fix_mA_routing.py +0 -0
  232. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_fix_mB_adapters.py +0 -0
  233. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_fix_mC_convert.py +0 -0
  234. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_fix_mD_logging_metrics.py +0 -0
  235. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_fix_mE_config_validation.py +0 -0
  236. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_fix_mF_ingress.py +0 -0
  237. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_gguf_introspect.py +0 -0
  238. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_guards_tool_loop.py +0 -0
  239. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_hardware.py +0 -0
  240. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_import_hygiene.py +0 -0
  241. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_ingress_anthropic.py +0 -0
  242. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_ingress_count_tokens.py +0 -0
  243. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_ingress_profile.py +0 -0
  244. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_language_tax.py +0 -0
  245. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_language_tax_integration.py +0 -0
  246. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_launcher_backend_variants.py +0 -0
  247. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_launcher_config_bench.py +0 -0
  248. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_launcher_devices_detect.py +0 -0
  249. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_launcher_devices_routes.py +0 -0
  250. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_launcher_gui_devices.py +0 -0
  251. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_launcher_gui_events.py +0 -0
  252. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_launcher_gui_readiness.py +0 -0
  253. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_launcher_mtp.py +0 -0
  254. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_launcher_mtp_fallback.py +0 -0
  255. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_launcher_provider_sync.py +0 -0
  256. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_launcher_readiness_restart.py +0 -0
  257. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_launcher_swap.py +0 -0
  258. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_launcher_swap_review.py +0 -0
  259. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_launcher_variant_config.py +0 -0
  260. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_launcher_variant_gui.py +0 -0
  261. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_launcher_variant_routes.py +0 -0
  262. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_memory_budget.py +0 -0
  263. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_memory_pressure.py +0 -0
  264. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_metrics_cache.py +0 -0
  265. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_metrics_collector.py +0 -0
  266. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_metrics_cost.py +0 -0
  267. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_metrics_endpoint.py +0 -0
  268. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_metrics_jsonl.py +0 -0
  269. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_metrics_prometheus.py +0 -0
  270. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_metrics_prometheus_cache.py +0 -0
  271. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_models_passthrough.py +0 -0
  272. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_openai_compat.py +0 -0
  273. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_openrouter_roster_diff.py +0 -0
  274. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_output_filters.py +0 -0
  275. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_output_filters_adapters.py +0 -0
  276. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_packaging.py +0 -0
  277. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_partial_stitch.py +0 -0
  278. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_plugin_adapter.py +0 -0
  279. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_plugins_integration.py +0 -0
  280. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_plugins_loader.py +0 -0
  281. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_plugins_registry.py +0 -0
  282. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_reasoning_strip.py +0 -0
  283. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_repair_byte_fallback.py +0 -0
  284. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_request_log.py +0 -0
  285. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_role_normalization.py +0 -0
  286. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_routing_adaptive.py +0 -0
  287. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_setup_sh.py +0 -0
  288. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_state_store.py +0 -0
  289. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_stop_reason_forward_compat.py +0 -0
  290. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_token_estimation.py +0 -0
  291. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_token_estimation_accurate.py +0 -0
  292. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_tokens_saved_metric.py +0 -0
  293. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_tool_repair.py +0 -0
  294. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_tool_repair_differential.py +0 -0
  295. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_tool_repair_lenient.py +0 -0
  296. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_tool_repair_r4.py +0 -0
  297. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_tool_repair_scanners.py +0 -0
  298. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_toolrepair_bench.py +0 -0
  299. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_translation_anthropic.py +0 -0
  300. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_translation_reverse.py +0 -0
  301. {coderouter_cli-2.12.0 → coderouter_cli-2.13.0}/tests/test_vscode_init.py +0 -0
@@ -9,6 +9,97 @@ are kept verbatim where the Japanese text itself is the subject).
9
9
 
10
10
  ---
11
11
 
12
+ ## [v2.13.0] — 2026-08-07 (remaining-high security hardening)
13
+
14
+ ### Changed (BREAKING)
15
+
16
+ - **Implicit `./providers.yaml` discovery is now opt-in, gated behind
17
+ `CODEROUTER_ALLOW_CWD_CONFIG`.** Previously, starting CodeRouter (or the
18
+ GUI launcher) from a directory containing a `providers.yaml` loaded that
19
+ file automatically. That was a code-execution vector: a hostile config
20
+ dropped into a repo could steer `restart_command`,
21
+ `launcher.backends[*].binary`, and `launcher.bench.command_template` —
22
+ all of which name executables — simply because of the working directory.
23
+ The current-working-directory step now runs only when
24
+ `CODEROUTER_ALLOW_CWD_CONFIG` is truthy (`1`/`true`/`yes`/`on`). When the
25
+ opt-in is off but a `./providers.yaml` exists and was not explicitly
26
+ named, a one-time `cwd-config-skipped` warning explains why it was
27
+ ignored and how to enable it (`--config`, `CODEROUTER_CONFIG`, or the
28
+ opt-in). To keep the old behaviour, set `CODEROUTER_ALLOW_CWD_CONFIG=1`
29
+ in directories you trust. (The v2.12.0 heads-up mis-stated the landing
30
+ release as "v2.12.0"; it actually lands here in v2.13.0.)
31
+ - **`restart_command` is now run as `subprocess.run(shlex.split(command),
32
+ shell=False)` — argv dispatch, no shell.** Self-healing previously ran
33
+ it through `shell=True`, so a `providers.yaml` from an untrusted source
34
+ meant arbitrary shell execution (directly, and unconditionally at
35
+ startup when excluded providers were restored from `state_dir`).
36
+ Pipelines, redirects, `~` expansion, and `FOO=bar cmd` env prefixes are
37
+ no longer supported. A value containing shell metacharacters (`&&`,
38
+ `||`, `|`, `;`, `>`, `<`, `$(...)`, `` ` ``) is now **refused (not run)**
39
+ rather than half-executed — under `shell=False`, `touch a; b` would
40
+ otherwise create a file literally named `a;` and report a "successful"
41
+ restart. Wrap shell-dependent commands in a script, or write them as
42
+ `/bin/sh -c '...'` explicitly.
43
+ - **`doctor --apply` now writes back to exactly the file the loader read.**
44
+ The apply path's config resolution was a hand-maintained copy of the
45
+ loader's search order; it now delegates to the loader's single
46
+ `resolve_config_path`, so the v2.13.0 CWD change can never make `--apply`
47
+ rewrite a file that `load_config` did not actually load.
48
+ - **The GUI launcher (`launcher_gui.py`) applies the same
49
+ `CODEROUTER_ALLOW_CWD_CONFIG` gate** to its own `providers.yaml`
50
+ discovery.
51
+ - **The launcher sweep API no longer accepts a `bench_command` (or
52
+ `results_dir`) from the request body.** `POST /api/launcher/sweep/start`
53
+ previously took an arbitrary command string, `shlex.split` it, and ran
54
+ it — so anyone who could reach the (loopback, token-optional) launcher
55
+ port could run any program under the CodeRouter process. The request now
56
+ carries only a `bench_preset` key that must name an entry under
57
+ `launcher.bench.presets` in `providers.yaml`; the executable template
58
+ lives solely in the operator-owned config, matching the trust boundary
59
+ already used for `launcher.backends[*].binary`. `bench_command` /
60
+ `results_dir` in a request are rejected with `400`. To keep a custom
61
+ sweep command, declare it under `launcher.bench.presets`; a single
62
+ `launcher.bench.command_template` still works unchanged as the implicit
63
+ `default` preset. Placeholder substitution in bench templates now splits
64
+ the template into argv *before* substituting, so a config label can no
65
+ longer inject extra arguments.
66
+
67
+ ### Fixed (security)
68
+
69
+ - **The launcher HTML no longer embeds `CODEROUTER_LAUNCHER_TOKEN`.**
70
+ `GET /launcher` used to substitute the shared secret straight into the
71
+ page, so `curl /launcher | grep` recovered it and defeated
72
+ `_require_launcher_token`. The page now receives only a boolean telling
73
+ the UI whether auth is enabled; the operator enters the token once and it
74
+ is held in `sessionStorage` (per-tab). Token-free deployments are
75
+ unaffected.
76
+ - **`POST /api/launcher/start` and `/sweep/start` now validate
77
+ `model_path` / `draft_model_path` against `launcher.model_dirs`** (the
78
+ same check previously applied only to `/suggest`). When `model_dirs` is
79
+ unset the endpoints stay open as before, with a one-time warning;
80
+ configure `model_dirs` when the launcher is reachable beyond loopback.
81
+ - **Stored XSS in the launcher process table is fixed.** Process-row
82
+ buttons no longer build `onclick` attributes from the process name
83
+ (which an HTML parser decoded back into executable JS); they use
84
+ `data-*` attributes with a single delegated listener, so a name like
85
+ `x');alert(1);//` renders as inert text.
86
+ - **The request body-size limit is now enforced for
87
+ `Transfer-Encoding: chunked` requests.** The middleware previously only
88
+ read `Content-Length`, so a chunked body bypassed the cap entirely (and
89
+ a malformed length header fell through as unlimited). It now also counts
90
+ bytes as they are received and aborts with `413` the moment the running
91
+ total exceeds the cap, without buffering the body.
92
+ - **The GUI bench-sweep window no longer orphans `llama-server`
93
+ processes.** Closing the sweep window (the `×` button) did not signal the
94
+ worker, and sweep child processes were never registered for the app's
95
+ bulk shutdown, so backends kept running and leaked VRAM and ports. The
96
+ window now stops the worker and its children on close (non-blocking, via
97
+ an `after()` poll with a grace period then `SIGKILL`), sweep children are
98
+ drained when the app closes, and the app confirms before quitting while a
99
+ sweep is running.
100
+
101
+ ---
102
+
12
103
  ## [v2.12.0] — 2026-08-04 (the context-budget guard actually fires now)
13
104
 
14
105
  **This release changes behaviour you can observe.** The token estimator
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coderouter-cli
3
- Version: 2.12.0
3
+ Version: 2.13.0
4
4
  Summary: Local-first, free-first, fallback-built-in LLM router. Claude Code / OpenAI compatible.
5
5
  Project-URL: Homepage, https://github.com/zephel01/CodeRouter
6
6
  Project-URL: Repository, https://github.com/zephel01/CodeRouter
@@ -600,28 +600,27 @@ def _run_check_model(args: argparse.Namespace) -> int:
600
600
 
601
601
 
602
602
  def _resolve_config_path(explicit: str | None) -> Path:
603
- """Mirror loader._candidate_paths and return the file actually used.
604
-
605
- Used by ``--apply`` to write back to the same path the loader
606
- picked up when it parsed providers.yaml. Falls through the same
607
- search order so a ``CODEROUTER_CONFIG`` env or default-path lookup
608
- matches the live config.
603
+ """Return the file the loader actually read, for ``--apply`` write-back.
604
+
605
+ Delegates the search order to
606
+ :func:`coderouter.config.loader.resolve_config_path` rather than
607
+ re-implementing it. Re-implementing it here is exactly how the
608
+ v2.13.0 CWD opt-in change would introduce a silent bug: if the loader
609
+ gates ``./providers.yaml`` behind ``CODEROUTER_ALLOW_CWD_CONFIG`` but
610
+ this copy still searched CWD unconditionally, ``doctor --apply`` would
611
+ write patches back into a file that ``load_config`` never read. Keeping
612
+ one source of truth makes that impossible.
613
+
614
+ When nothing exists, falls back to ``~/.coderouter/providers.yaml``
615
+ (the loader's last candidate) — the apply step surfaces a clearer
616
+ error against that path than this resolver would.
609
617
  """
610
- import os
611
-
612
- candidates: list[Path] = []
613
- if explicit:
614
- candidates.append(Path(explicit))
615
- if env_path := os.environ.get("CODEROUTER_CONFIG"):
616
- candidates.append(Path(env_path))
617
- candidates.append(Path.cwd() / "providers.yaml")
618
- candidates.append(Path.home() / ".coderouter" / "providers.yaml")
619
- for p in candidates:
620
- if p.is_file():
621
- return p
622
- # Fall back to the last candidate even if absent — the apply step
623
- # will surface a clearer error than this resolver would.
624
- return candidates[-1]
618
+ from coderouter.config.loader import resolve_config_path
619
+
620
+ resolved = resolve_config_path(explicit)
621
+ if resolved is not None:
622
+ return resolved
623
+ return Path.home() / ".coderouter" / "providers.yaml"
625
624
 
626
625
 
627
626
  def _write_verdict_line(*, write: bool, files_written: int) -> None:
@@ -0,0 +1,264 @@
1
+ """Load and validate CodeRouter configuration.
2
+
3
+ Search order (first hit wins):
4
+ 1. Path passed explicitly (CLI --config flag)
5
+ 2. $CODEROUTER_CONFIG env var
6
+ 3. ./providers.yaml (current working dir) — opt-in since v2.13.0,
7
+ gated behind ``CODEROUTER_ALLOW_CWD_CONFIG`` (truthy: 1/true/yes/on)
8
+ 4. ~/.coderouter/providers.yaml
9
+
10
+ Secrets are resolved by reading the env var named by `api_key_env`.
11
+
12
+ .. note::
13
+ **v2.13.0 (security):** step 3 above (implicit CWD discovery) is now
14
+ opt-in, gated behind ``CODEROUTER_ALLOW_CWD_CONFIG``. It was a code
15
+ execution vector: a hostile ``providers.yaml`` dropped into a repo
16
+ could steer ``restart_command`` / ``launcher.backends[*].binary`` /
17
+ ``launcher.bench.command_template`` — all of which name executables —
18
+ simply because CodeRouter happened to be started from that directory.
19
+ When the opt-in is enabled and step 3 resolves the config, a one-time
20
+ ``cwd-config-loaded`` warning fires (never when an explicit
21
+ ``--config`` / ``CODEROUTER_CONFIG`` path was used, even if it happens
22
+ to point at the same file). When the opt-in is NOT set but a
23
+ ``./providers.yaml`` exists and was not explicitly named, a one-time
24
+ ``cwd-config-skipped`` warning fires so the operator understands why
25
+ that file was ignored.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import os
31
+ from pathlib import Path
32
+
33
+ import yaml
34
+
35
+ from coderouter.config.schemas import CodeRouterConfig
36
+ from coderouter.logging import get_logger
37
+
38
+ logger = get_logger(__name__)
39
+
40
+ # Name of the env var that opts CWD ``providers.yaml`` discovery back in
41
+ # (see module docstring). v2.13.0 gates step 3 of the search behind it.
42
+ CWD_CONFIG_ENV = "CODEROUTER_ALLOW_CWD_CONFIG"
43
+ # Truthy vocabulary shared by every CWD-opt-in / ALLOW_PAID style toggle.
44
+ _TRUTHY = {"1", "true", "yes", "on"}
45
+
46
+ # Guards the one-time ``cwd-config-loaded`` warning (opt-in enabled and the
47
+ # CWD step actually resolved) so a process that calls load_config()
48
+ # repeatedly (tests, hot-reload, multiple create_app() calls) only logs it
49
+ # once.
50
+ _cwd_config_warning_emitted = False
51
+ # Guards the one-time ``cwd-config-skipped`` warning (opt-in NOT set but a
52
+ # ./providers.yaml exists and was ignored) — same once-per-process rationale.
53
+ _cwd_config_skip_warning_emitted = False
54
+
55
+
56
+ def cwd_config_allowed() -> bool:
57
+ """True iff the CWD ``providers.yaml`` opt-in is enabled (v2.13.0)."""
58
+ return os.environ.get(CWD_CONFIG_ENV, "").strip().lower() in _TRUTHY
59
+
60
+
61
+ def _candidate_paths(explicit: str | os.PathLike[str] | None) -> list[Path]:
62
+ paths: list[Path] = []
63
+ if explicit:
64
+ paths.append(Path(explicit))
65
+ if env_path := os.environ.get("CODEROUTER_CONFIG"):
66
+ paths.append(Path(env_path))
67
+ # v2.13.0: implicit CWD discovery is opt-in — a hostile providers.yaml
68
+ # dropped into a repo could otherwise steer restart_command / launcher
69
+ # binaries simply because CodeRouter was started from that directory.
70
+ if cwd_config_allowed():
71
+ paths.append(Path.cwd() / "providers.yaml")
72
+ paths.append(Path.home() / ".coderouter" / "providers.yaml")
73
+ return paths
74
+
75
+
76
+ def resolve_config_path(
77
+ explicit: str | os.PathLike[str] | None = None,
78
+ ) -> Path | None:
79
+ """Return the config file the loader would pick, or None if none exists.
80
+
81
+ Single source of truth for the search order so callers (e.g.
82
+ ``coderouter.cli._resolve_config_path`` driving ``doctor --apply``)
83
+ never re-implement it. Re-implementing it risks writing back to a
84
+ different file than the one :func:`load_config` actually read — the
85
+ exact regression the v2.13.0 CWD opt-in would otherwise introduce.
86
+ """
87
+ return next((p for p in _candidate_paths(explicit) if p.is_file()), None)
88
+
89
+
90
+ def _explicitly_named(
91
+ path: Path, explicit: str | os.PathLike[str] | None
92
+ ) -> bool:
93
+ """True iff ``path`` matches the literal --config or CODEROUTER_CONFIG value.
94
+
95
+ Used to keep the CWD warnings quiet when ``./providers.yaml`` was
96
+ chosen on purpose (an explicit choice, however coincidental, is not
97
+ the implicit-discovery behaviour being gated).
98
+ """
99
+ if explicit and Path(explicit) == path:
100
+ return True
101
+ env_path = os.environ.get("CODEROUTER_CONFIG")
102
+ return env_path is not None and Path(env_path) == path
103
+
104
+
105
+ def _warn_if_cwd_config(
106
+ chosen: Path, *, explicit: str | os.PathLike[str] | None
107
+ ) -> None:
108
+ """Emit the one-time ``cwd-config-loaded`` warning (opt-in is enabled).
109
+
110
+ Only fires when ``chosen`` was resolved via the *implicit* CWD-search
111
+ step — which, since v2.13.0, only runs when
112
+ ``CODEROUTER_ALLOW_CWD_CONFIG`` is set. Compares ``chosen`` against the
113
+ *literal* explicit / env candidate paths (not merely whether those
114
+ inputs were set) so that an explicit ``--config`` (or
115
+ ``CODEROUTER_CONFIG``) path that happens to coincide with
116
+ ``./providers.yaml`` stays quiet, while an explicit path/env var that
117
+ was set but did NOT resolve (file missing, search fell through to CWD)
118
+ still warns — the CWD step is what actually served the config there.
119
+ """
120
+ global _cwd_config_warning_emitted
121
+ if _cwd_config_warning_emitted:
122
+ return
123
+ cwd_path = Path.cwd() / "providers.yaml"
124
+ if chosen != cwd_path:
125
+ return
126
+ if _explicitly_named(chosen, explicit):
127
+ return
128
+
129
+ _cwd_config_warning_emitted = True
130
+ logger.warning(
131
+ "cwd-config-loaded",
132
+ extra={
133
+ "path": str(chosen),
134
+ "hint": (
135
+ f"loaded providers.yaml from the current working "
136
+ f"directory ({chosen}) because CODEROUTER_ALLOW_CWD_CONFIG "
137
+ "opt-in is enabled. This file's restart_command / "
138
+ "launcher.backends[*].binary / "
139
+ "launcher.bench.command_template decide which executables "
140
+ "run — only enable the opt-in in directories you trust."
141
+ ),
142
+ },
143
+ )
144
+
145
+
146
+ def _warn_if_cwd_config_skipped(
147
+ explicit: str | os.PathLike[str] | None,
148
+ ) -> Path | None:
149
+ """Warn once when a ./providers.yaml exists but was skipped (no opt-in).
150
+
151
+ Returns the skipped CWD path (so :func:`load_config` can name it in a
152
+ later ``FileNotFoundError``), or None when there is nothing to skip:
153
+ the opt-in is enabled, no ``./providers.yaml`` exists, or the file was
154
+ explicitly named (``--config`` / ``CODEROUTER_CONFIG``) — in which case
155
+ it is not being skipped at all.
156
+ """
157
+ global _cwd_config_skip_warning_emitted
158
+ if cwd_config_allowed():
159
+ return None
160
+ cwd_path = Path.cwd() / "providers.yaml"
161
+ if not cwd_path.is_file():
162
+ return None
163
+ if _explicitly_named(cwd_path, explicit):
164
+ return None
165
+
166
+ if not _cwd_config_skip_warning_emitted:
167
+ _cwd_config_skip_warning_emitted = True
168
+ logger.warning(
169
+ "cwd-config-skipped",
170
+ extra={
171
+ "path": str(cwd_path),
172
+ "hint": (
173
+ f"found providers.yaml in the current working "
174
+ f"directory ({cwd_path}) but did NOT load it: implicit "
175
+ "CWD discovery is opt-in since v2.13.0 (a hostile "
176
+ "providers.yaml can steer restart_command / launcher "
177
+ "binaries). To use it, set CODEROUTER_ALLOW_CWD_CONFIG=1 "
178
+ "(only in directories you trust) or pass --config "
179
+ f"{cwd_path} to name it explicitly."
180
+ ),
181
+ },
182
+ )
183
+ return cwd_path
184
+
185
+
186
+ def load_config(path: str | os.PathLike[str] | None = None) -> CodeRouterConfig:
187
+ """Load providers.yaml + apply ALLOW_PAID env override."""
188
+ skipped_cwd = _warn_if_cwd_config_skipped(path)
189
+ candidates = _candidate_paths(path)
190
+ chosen: Path | None = next((p for p in candidates if p.is_file()), None)
191
+ if chosen is None:
192
+ searched = "\n ".join(str(p) for p in candidates)
193
+ message = (
194
+ f"providers.yaml not found. Searched:\n {searched}\n"
195
+ f"Hint: copy examples/providers.yaml to ~/.coderouter/providers.yaml"
196
+ )
197
+ if skipped_cwd is not None:
198
+ message += (
199
+ f"\nNote: {skipped_cwd} exists but was NOT read — implicit "
200
+ "CWD discovery is opt-in since v2.13.0. Three ways to use "
201
+ f"it: pass --config {skipped_cwd}, set "
202
+ f"CODEROUTER_CONFIG={skipped_cwd}, or set "
203
+ "CODEROUTER_ALLOW_CWD_CONFIG=1 (only in directories you trust)."
204
+ )
205
+ raise FileNotFoundError(message)
206
+ _warn_if_cwd_config(chosen, explicit=path)
207
+
208
+ with chosen.open("r", encoding="utf-8") as f:
209
+ raw = yaml.safe_load(f) or {}
210
+
211
+ # v0.6-A: CODEROUTER_MODE env overrides the YAML default_profile BEFORE
212
+ # initial validation, so that (a) a typo'd file default that would otherwise
213
+ # fail can be rescued by an explicit env-set mode, and (b) the model-
214
+ # validator's "default_profile must exist in profiles" check applies to the
215
+ # *effective* mode the engine will see, not the pre-override YAML value.
216
+ #
217
+ # v1.8.0+: also resolve env_mode through ``mode_aliases`` before assigning,
218
+ # so that startup-time ``--mode coding`` (env CODEROUTER_MODE=coding)
219
+ # behaves symmetrically with the runtime ``X-CodeRouter-Mode: coding``
220
+ # header — both should accept short intent names like ``coding`` /
221
+ # ``general`` / ``reasoning`` and resolve them to the underlying profile
222
+ # (e.g. ``claude-code-nim`` in providers.nvidia-nim.yaml). Without this,
223
+ # users on the NIM example yaml hit
224
+ # "default_profile 'coding' is not declared in profiles:
225
+ # known=['claude-code-nim', ...]"
226
+ # because mode_aliases only fired at request time, not at startup.
227
+ env_mode = os.environ.get("CODEROUTER_MODE", "").strip()
228
+ if env_mode:
229
+ # Pre-validation alias resolution: if env_mode isn't directly a
230
+ # profile name but matches an entry in raw["mode_aliases"], swap it
231
+ # for the underlying profile name. This avoids forcing every example
232
+ # yaml to mirror the v1.8.0 four-profile names (multi/coding/general
233
+ # /reasoning) just to accept the canonical short --mode flags.
234
+ raw_profiles = raw.get("profiles", []) or []
235
+ profile_names = {
236
+ p.get("name") for p in raw_profiles if isinstance(p, dict)
237
+ }
238
+ raw_aliases = raw.get("mode_aliases", {}) or {}
239
+ if (
240
+ env_mode not in profile_names
241
+ and isinstance(raw_aliases, dict)
242
+ and env_mode in raw_aliases
243
+ ):
244
+ env_mode = raw_aliases[env_mode]
245
+ raw["default_profile"] = env_mode
246
+
247
+ config = CodeRouterConfig.model_validate(raw)
248
+
249
+ # Env var ALLOW_PAID overrides file value (so users can flip it per-shell)
250
+ env_paid = os.environ.get("ALLOW_PAID", "").strip().lower()
251
+ if env_paid in {"1", "true", "yes", "on"}:
252
+ config.allow_paid = True
253
+ elif env_paid in {"0", "false", "no", "off"}:
254
+ config.allow_paid = False
255
+
256
+ return config
257
+
258
+
259
+ def resolve_api_key(api_key_env: str | None) -> str | None:
260
+ """Look up an API key from the named env var. Returns None if unset."""
261
+ if not api_key_env:
262
+ return None
263
+ value = os.environ.get(api_key_env, "").strip()
264
+ return value or None
@@ -355,15 +355,20 @@ class AgentCliConfig(BaseModel):
355
355
  return self
356
356
 
357
357
 
358
- # [Unreleased]/planned v2.12.0: patterns used by ProviderConfig's
358
+ # v2.13.0: patterns used by ProviderConfig's
359
359
  # ``_warn_restart_command_shell_syntax`` validator to flag
360
- # ``restart_command`` values that rely on shell syntax not honored by
361
- # the planned ``shlex.split`` + ``shell=False`` dispatch (see that
362
- # validator's docstring and the field description below for the full
363
- # rationale). Compiled once at module scope since every ProviderConfig
364
- # validation reuses them.
365
- _RESTART_COMMAND_SHELL_META_RE = re.compile(r"(&&|\|\||\||;|>|<|\$\(|`)")
366
- _RESTART_COMMAND_ENV_PREFIX_RE = re.compile(r"^\w+=")
360
+ # ``restart_command`` values that rely on shell syntax not honored by the
361
+ # ``shlex.split`` + ``shell=False`` dispatch (see that validator's
362
+ # docstring and the field description below for the full rationale).
363
+ # Public (no leading underscore) because ``coderouter.guards.self_healing``
364
+ # imports ``RESTART_COMMAND_SHELL_META_RE`` to *refuse* to run any
365
+ # restart_command containing shell metacharacters — under shell=False a
366
+ # value like ``touch a; b`` would create a file literally named ``a;`` and
367
+ # exit 0, i.e. report a "successful" restart while only half-executing.
368
+ # Compiled once at module scope since every ProviderConfig validation
369
+ # reuses them.
370
+ RESTART_COMMAND_SHELL_META_RE = re.compile(r"(&&|\|\||\||;|>|<|\$\(|`)")
371
+ RESTART_COMMAND_ENV_PREFIX_RE = re.compile(r"^\w+=")
367
372
 
368
373
 
369
374
  class ProviderConfig(BaseModel):
@@ -520,54 +525,56 @@ class ProviderConfig(BaseModel):
520
525
  "transitions to UNHEALTHY. Unset = no automatic restart "
521
526
  "(recovery probe still runs, waiting for manual restart). "
522
527
  "\n\n"
523
- "[Unreleased]/planned v2.12.0: currently run as "
524
- "``subprocess.run(command, shell=True)`` (coderouter/guards/"
525
- "self_healing.py), so shell syntax works today — pipelines "
526
- "(``pkill ollama && ollama serve``), ``~`` expansion, and "
527
- "leading env-var assignments (``OLLAMA_HOST=0.0.0.0 ollama "
528
- "serve``) are all honored. A future minor switches this to "
529
- "``shlex.split(command)`` + ``shell=False`` (argv dispatch, "
530
- "no shell in between), which does NOT support any of the "
531
- "above — ``&&`` becomes a literal argument to the first "
532
- "command instead of a separator, ``~/...`` paths are not "
533
- "expanded, and ``FOO=bar cmd`` fails to exec. If your "
534
- "command needs shell features, wrap it in a small script "
535
- "and point restart_command at that script instead, e.g. "
536
- "``restart_command: /path/to/restart-ollama.sh``."
528
+ "v2.13.0: run as ``subprocess.run(shlex.split(command), "
529
+ "shell=False)`` (coderouter/guards/self_healing.py) — argv "
530
+ "dispatch, no shell in between. Pipelines "
531
+ "(``pkill ollama && ollama serve``), redirects, ``~`` "
532
+ "expansion, and leading env-var assignments "
533
+ "(``OLLAMA_HOST=0.0.0.0 ollama serve``) are NOT supported: "
534
+ "``~/...`` paths are not expanded and ``FOO=bar cmd`` fails to "
535
+ "exec. A value containing shell metacharacters (``&&``, ``||``, "
536
+ "``|``, ``;``, ``>``, ``<``, ``$(...)``, ```...```) is refused "
537
+ "outright (not run) rather than half-executed. If your command "
538
+ "needs shell features, wrap it in a small script and point "
539
+ "restart_command at that script (e.g. "
540
+ "``restart_command: /path/to/restart-ollama.sh``), or write it "
541
+ "as ``/bin/sh -c '...'`` explicitly."
537
542
  ),
538
543
  )
539
544
 
540
545
  @model_validator(mode="after")
541
546
  def _warn_restart_command_shell_syntax(self) -> ProviderConfig:
542
- """[Unreleased]/planned v2.12.0 breaking-change heads-up (warning only).
543
-
544
- ``coderouter/guards/self_healing.py`` currently runs
545
- ``restart_command`` via ``subprocess.run(command, shell=True)``.
546
- The planned v2.12.0 change switches that to
547
- ``shlex.split(command)`` + ``shell=False`` — a shell-syntax
548
- ``restart_command`` that works fine today would then either
549
- crash (``FileNotFoundError`` for ``~/...`` paths or an
550
- ``ENV=val cmd`` prefix) or, worse, silently misbehave (``&&`` /
551
- ``||`` / ``|`` / ``;`` / redirects becoming literal argv tokens
552
- instead of shell operators — a provider "restart" that quietly
553
- does the wrong thing is worse than one that errors loudly).
547
+ """v2.13.0: warn that this ``restart_command`` will not run as written.
548
+
549
+ ``coderouter/guards/self_healing.py`` runs ``restart_command`` via
550
+ ``subprocess.run(shlex.split(command), shell=False)`` — argv
551
+ dispatch, no shell. A shell-syntax ``restart_command`` therefore
552
+ does NOT do what the shell would do:
553
+
554
+ * shell metacharacters (``&&`` / ``||`` / ``|`` / ``;`` /
555
+ redirects / ``$(...)`` / backticks) → the value is *refused*
556
+ (not run) by self_healing, because under shell=False they would
557
+ otherwise become literal argv tokens and half-execute a
558
+ provider "restart" that quietly does the wrong thing;
559
+ * a leading ``~/...`` path or an ``ENV=val cmd`` prefix →
560
+ ``exec`` fails (``FileNotFoundError``) because there is no shell
561
+ to expand ``~`` or apply the assignment.
554
562
 
555
563
  Deliberately never raises: this validator does not enforce
556
- anything, it only warns ahead of the wire change so operators
557
- can migrate ``restart_command`` to a plain argv (or a wrapper
558
- script) before v2.12.0 lands, instead of discovering the break
559
- the next time a provider actually goes UNHEALTHY.
564
+ anything, it only warns so an existing providers.yaml still loads
565
+ (rather than becoming un-startable). Migrate ``restart_command`` to
566
+ a plain argv, a wrapper script, or an explicit ``/bin/sh -c '...'``.
560
567
  """
561
568
  cmd = self.restart_command
562
569
  if not cmd:
563
570
  return self
564
571
  stripped = cmd.strip()
565
572
  reasons: list[str] = []
566
- if _RESTART_COMMAND_SHELL_META_RE.search(cmd):
573
+ if RESTART_COMMAND_SHELL_META_RE.search(cmd):
567
574
  reasons.append("shell metacharacter (&&, ||, |, ;, >, <, $(...), or `...`)")
568
575
  if stripped.startswith("~/"):
569
576
  reasons.append("leading '~/' path (no shell expansion under shell=False)")
570
- if _RESTART_COMMAND_ENV_PREFIX_RE.match(stripped):
577
+ if RESTART_COMMAND_ENV_PREFIX_RE.match(stripped):
571
578
  reasons.append("leading environment-variable assignment (FOO=bar ...)")
572
579
  if reasons:
573
580
  logger.warning(
@@ -577,13 +584,13 @@ class ProviderConfig(BaseModel):
577
584
  "restart_command": cmd,
578
585
  "reasons": reasons,
579
586
  "hint": (
580
- "restart_command currently runs via "
581
- "subprocess.run(..., shell=True); a planned "
582
- "v2.12.0 change switches to shlex.split(...) + "
583
- "shell=False, which does not support this "
584
- "command's shell syntax. Rewrite it as plain "
585
- "argv, or wrap it in a script and point "
586
- "restart_command at that script."
587
+ "restart_command runs via "
588
+ "subprocess.run(shlex.split(...), shell=False) as of "
589
+ "v2.13.0, which does not support this command's "
590
+ "shell syntax: values with shell metacharacters are "
591
+ "refused (not run), and '~/' / 'FOO=bar' prefixes "
592
+ "fail to exec. Rewrite it as plain argv, wrap it in "
593
+ "a script, or use /bin/sh -c '...'."
587
594
  ),
588
595
  },
589
596
  )
@@ -1573,6 +1580,28 @@ class LauncherOptionProfile(BaseModel):
1573
1580
  )
1574
1581
 
1575
1582
 
1583
+ class LauncherBenchPreset(BaseModel):
1584
+ """launcher.bench.presets[<key>] の1エントリ。H-2: スイープAPIが受け付けるのはキー名だけ。"""
1585
+
1586
+ model_config = ConfigDict(extra="forbid")
1587
+ name: str = Field(..., min_length=1, description="Launcher UI に出す表示名。")
1588
+ command_template: str = Field(
1589
+ ...,
1590
+ min_length=1,
1591
+ description="外部ベンチコマンドのテンプレ。{port} {config} {base_url} {results_dir} {runs} を単純置換で展開。",
1592
+ )
1593
+ runs: int | None = Field(
1594
+ default=None,
1595
+ ge=1,
1596
+ le=1000,
1597
+ description="このプリセット固有の実行回数。None なら launcher.bench.runs。",
1598
+ )
1599
+ results_dir: str | None = Field(
1600
+ default=None,
1601
+ description="このプリセット固有の results ディレクトリ。None なら launcher.bench.results_dir。リクエストからは指定できない。",
1602
+ )
1603
+
1604
+
1576
1605
  class LauncherBenchConfig(BaseModel):
1577
1606
  """The ``launcher.bench:`` block — defaults for the bench sweep feature.
1578
1607
 
@@ -1614,6 +1643,23 @@ class LauncherBenchConfig(BaseModel):
1614
1643
  "大きな GGUF ロードを考慮した既定 5 分。"
1615
1644
  ),
1616
1645
  )
1646
+ presets: dict[str, LauncherBenchPreset] = Field(
1647
+ default_factory=dict,
1648
+ description="名前付きベンチコマンド。キーが POST /api/launcher/sweep/start の bench_preset が取りうる唯一の値。",
1649
+ )
1650
+ default_preset: str | None = Field(
1651
+ default=None,
1652
+ description="bench_preset 未指定時に使うプリセットキー。None なら command_template 由来の暗黙 default。",
1653
+ )
1654
+
1655
+ @model_validator(mode="after")
1656
+ def _check_default_preset(self) -> Self:
1657
+ if self.default_preset is not None and self.default_preset not in self.presets:
1658
+ raise ValueError(
1659
+ f"launcher.bench.default_preset={self.default_preset!r} is not a key of "
1660
+ f"launcher.bench.presets (known: {sorted(self.presets) or 'none'})"
1661
+ )
1662
+ return self
1617
1663
 
1618
1664
 
1619
1665
  class SwapModelSpec(BaseModel):