coderouter-cli 2.7.7__tar.gz → 2.7.8__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 (262) hide show
  1. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/CHANGELOG.md +88 -0
  2. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/PKG-INFO +1 -1
  3. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/adapters/agent_cli.py +284 -95
  4. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/config/schemas.py +44 -8
  5. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/guards/backend_health.py +63 -1
  6. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/logging.py +60 -0
  7. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/routing/fallback.py +37 -0
  8. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/README.en.md +1 -0
  9. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/README.md +1 -0
  10. coderouter_cli-2.7.8/docs/backends/external-agents.en.md +233 -0
  11. coderouter_cli-2.7.8/docs/backends/external-agents.md +233 -0
  12. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/designs/external-agents-adapter.md +20 -3
  13. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/examples/providers-agent-cli.yaml +71 -44
  14. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/pyproject.toml +1 -1
  15. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_agent_cli.py +357 -18
  16. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_backend_health.py +182 -0
  17. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fix_mE_config_validation.py +41 -0
  18. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/.gitignore +0 -0
  19. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/LICENSE +0 -0
  20. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/README.en.md +0 -0
  21. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/README.md +0 -0
  22. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/__init__.py +0 -0
  23. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/__main__.py +0 -0
  24. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/adapters/__init__.py +0 -0
  25. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/adapters/anthropic_native.py +0 -0
  26. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/adapters/base.py +0 -0
  27. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/adapters/openai_compat.py +0 -0
  28. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/adapters/registry.py +0 -0
  29. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/cli.py +0 -0
  30. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/cli_stats.py +0 -0
  31. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/config/__init__.py +0 -0
  32. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/config/capability_registry.py +0 -0
  33. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/config/env_file.py +0 -0
  34. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/config/loader.py +0 -0
  35. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/cost.py +0 -0
  36. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/data/__init__.py +0 -0
  37. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/data/model-capabilities.yaml +0 -0
  38. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/doctor.py +0 -0
  39. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/doctor_apply.py +0 -0
  40. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/env_security.py +0 -0
  41. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/errors.py +0 -0
  42. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/gguf_introspect.py +0 -0
  43. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/guards/__init__.py +0 -0
  44. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/guards/_fingerprint.py +0 -0
  45. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/guards/context_budget.py +0 -0
  46. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/guards/continuous_probe.py +0 -0
  47. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/guards/drift_actions.py +0 -0
  48. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/guards/drift_detection.py +0 -0
  49. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/guards/memory_budget.py +0 -0
  50. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/guards/memory_pressure.py +0 -0
  51. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/guards/self_healing.py +0 -0
  52. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/guards/tool_loop.py +0 -0
  53. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/hardware.py +0 -0
  54. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/ingress/__init__.py +0 -0
  55. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/ingress/anthropic_routes.py +0 -0
  56. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/ingress/app.py +0 -0
  57. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/ingress/dashboard_routes.py +0 -0
  58. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/ingress/launcher_routes.py +0 -0
  59. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/ingress/metrics_routes.py +0 -0
  60. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/ingress/openai_routes.py +0 -0
  61. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/language_tax.py +0 -0
  62. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/launcher_speculative.py +0 -0
  63. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/metrics/__init__.py +0 -0
  64. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/metrics/collector.py +0 -0
  65. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/metrics/prometheus.py +0 -0
  66. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/output_filters.py +0 -0
  67. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/plugins/__init__.py +0 -0
  68. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/plugins/base.py +0 -0
  69. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/plugins/loader.py +0 -0
  70. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/plugins/registry.py +0 -0
  71. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/routing/__init__.py +0 -0
  72. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/routing/adaptive.py +0 -0
  73. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/routing/auto_router.py +0 -0
  74. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/routing/budget.py +0 -0
  75. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/routing/capability.py +0 -0
  76. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/state/__init__.py +0 -0
  77. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/state/audit_log.py +0 -0
  78. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/state/replay.py +0 -0
  79. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/state/request_log.py +0 -0
  80. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/state/store.py +0 -0
  81. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/state/suggest_rules.py +0 -0
  82. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/token_estimation.py +0 -0
  83. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/token_estimation_accurate.py +0 -0
  84. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/translation/__init__.py +0 -0
  85. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/translation/anthropic.py +0 -0
  86. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/translation/convert.py +0 -0
  87. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/translation/tool_repair.py +0 -0
  88. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/assets/dashboard-demo.png +0 -0
  89. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/backends/claude-code-llamacpp-vllm.en.md +0 -0
  90. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/backends/claude-code-llamacpp-vllm.md +0 -0
  91. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/backends/gguf_dl.en.md +0 -0
  92. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/backends/gguf_dl.md +0 -0
  93. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/backends/hf-ollama-models.en.md +0 -0
  94. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/backends/hf-ollama-models.md +0 -0
  95. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/backends/install-backends.en.md +0 -0
  96. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/backends/install-backends.md +0 -0
  97. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/backends/launcher-quickstart.en.md +0 -0
  98. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/backends/launcher-quickstart.md +0 -0
  99. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/backends/launcher.en.md +0 -0
  100. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/backends/launcher.md +0 -0
  101. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/backends/llamacpp-direct.en.md +0 -0
  102. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/backends/llamacpp-direct.md +0 -0
  103. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/backends/lmstudio-direct.en.md +0 -0
  104. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/backends/lmstudio-direct.md +0 -0
  105. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/backends/verify-ollama-0.23.1.md +0 -0
  106. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/concepts/architecture.en.md +0 -0
  107. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/concepts/architecture.md +0 -0
  108. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/concepts/context-budget.en.md +0 -0
  109. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/concepts/context-budget.md +0 -0
  110. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/concepts/continuous-probing.en.md +0 -0
  111. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/concepts/continuous-probing.md +0 -0
  112. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/concepts/drift-detection.en.md +0 -0
  113. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/concepts/drift-detection.md +0 -0
  114. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/concepts/partial-stitch.en.md +0 -0
  115. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/concepts/partial-stitch.md +0 -0
  116. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/designs/v1.5-dashboard-mockup.html +0 -0
  117. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/designs/v1.6-auto-router-verification.md +0 -0
  118. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/designs/v1.6-auto-router.md +0 -0
  119. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/guides/free-tier-guide.en.md +0 -0
  120. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/guides/free-tier-guide.md +0 -0
  121. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/guides/language-tax.en.md +0 -0
  122. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/guides/language-tax.md +0 -0
  123. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/guides/remote-access.en.md +0 -0
  124. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/guides/remote-access.md +0 -0
  125. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/guides/security.en.md +0 -0
  126. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/guides/security.md +0 -0
  127. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/guides/troubleshooting.en.md +0 -0
  128. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/guides/troubleshooting.md +0 -0
  129. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/guides/usage-guide.en.md +0 -0
  130. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/guides/usage-guide.md +0 -0
  131. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/low-memory-integration.en.md +0 -0
  132. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/low-memory-integration.md +0 -0
  133. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/openrouter-roster/CHANGES.md +0 -0
  134. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/openrouter-roster/README.md +0 -0
  135. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/openrouter-roster/latest.json +0 -0
  136. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/retrospectives/v0.4.md +0 -0
  137. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/retrospectives/v0.5-verify.md +0 -0
  138. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/retrospectives/v0.5.md +0 -0
  139. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/retrospectives/v0.6.md +0 -0
  140. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/retrospectives/v0.7.md +0 -0
  141. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/retrospectives/v1.0-verify.md +0 -0
  142. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/retrospectives/v1.0.md +0 -0
  143. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/start/quickstart.en.md +0 -0
  144. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/start/quickstart.md +0 -0
  145. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/start/when-do-i-need-coderouter.en.md +0 -0
  146. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/start/when-do-i-need-coderouter.md +0 -0
  147. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/examples/.env.example +0 -0
  148. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/examples/README.md +0 -0
  149. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/examples/providers-multiagent.yaml +0 -0
  150. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/examples/providers.context-budget-test.yaml +0 -0
  151. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/examples/providers.llamacpp-vllm.yaml +0 -0
  152. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/examples/providers.nvidia-nim.yaml +0 -0
  153. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/examples/providers.ollama-auto-custom.yaml +0 -0
  154. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/examples/providers.ollama-auto.yaml +0 -0
  155. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/examples/providers.ollama-free-chain.yaml +0 -0
  156. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/examples/providers.raspberrypi.yaml +0 -0
  157. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/examples/providers.yaml +0 -0
  158. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/scripts/demo_traffic.sh +0 -0
  159. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/scripts/openrouter_roster_diff.py +0 -0
  160. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/scripts/smoke_v2_2.sh +0 -0
  161. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/scripts/verify-providers.yaml +0 -0
  162. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/scripts/verify_ollama_0_23.py +0 -0
  163. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/scripts/verify_v0_5.sh +0 -0
  164. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/scripts/verify_v1_0.sh +0 -0
  165. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/__init__.py +0 -0
  166. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/conftest.py +0 -0
  167. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_adapter_anthropic.py +0 -0
  168. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_audit_log.py +0 -0
  169. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_auto_router.py +0 -0
  170. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_auto_router_cjk.py +0 -0
  171. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_budget.py +0 -0
  172. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_capability.py +0 -0
  173. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_capability_degraded_payload.py +0 -0
  174. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_capability_registry.py +0 -0
  175. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_capability_registry_cache_control.py +0 -0
  176. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_capability_tool_choice.py +0 -0
  177. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_claude_code_suitability.py +0 -0
  178. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_cli.py +0 -0
  179. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_cli_bind_warning.py +0 -0
  180. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_cli_stats.py +0 -0
  181. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_config.py +0 -0
  182. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_config_shim_actions.py +0 -0
  183. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_context_budget.py +0 -0
  184. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_continuous_probe.py +0 -0
  185. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_dashboard_endpoint.py +0 -0
  186. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_doctor.py +0 -0
  187. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_doctor_apply.py +0 -0
  188. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_doctor_cache_probe.py +0 -0
  189. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_drift_actions.py +0 -0
  190. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_drift_detection.py +0 -0
  191. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_drift_detection_integration.py +0 -0
  192. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_env_file.py +0 -0
  193. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_env_security.py +0 -0
  194. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_errors.py +0 -0
  195. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_examples_yaml.py +0 -0
  196. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fallback.py +0 -0
  197. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fallback_anthropic.py +0 -0
  198. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fallback_cache_control.py +0 -0
  199. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fallback_cache_observed.py +0 -0
  200. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fallback_cache_strip.py +0 -0
  201. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fallback_empty_response.py +0 -0
  202. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fallback_misconfig_warn.py +0 -0
  203. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fallback_paid_gate.py +0 -0
  204. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fallback_thinking.py +0 -0
  205. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fallback_tool_choice_emulate.py +0 -0
  206. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fix_h1_prometheus_drift.py +0 -0
  207. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fix_h2_h6_translation.py +0 -0
  208. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fix_h3_client_reuse.py +0 -0
  209. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fix_h4_h5_guards.py +0 -0
  210. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fix_h7_probe_restore.py +0 -0
  211. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fix_h8_launcher_auth.py +0 -0
  212. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fix_mA_routing.py +0 -0
  213. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fix_mB_adapters.py +0 -0
  214. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fix_mC_convert.py +0 -0
  215. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fix_mD_logging_metrics.py +0 -0
  216. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fix_mF_ingress.py +0 -0
  217. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_gguf_introspect.py +0 -0
  218. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_guards_tool_loop.py +0 -0
  219. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_hardware.py +0 -0
  220. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_ingress_anthropic.py +0 -0
  221. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_ingress_count_tokens.py +0 -0
  222. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_ingress_profile.py +0 -0
  223. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_language_tax.py +0 -0
  224. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_language_tax_integration.py +0 -0
  225. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_launcher_mtp.py +0 -0
  226. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_launcher_mtp_fallback.py +0 -0
  227. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_launcher_provider_sync.py +0 -0
  228. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_memory_budget.py +0 -0
  229. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_memory_pressure.py +0 -0
  230. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_metrics_cache.py +0 -0
  231. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_metrics_collector.py +0 -0
  232. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_metrics_cost.py +0 -0
  233. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_metrics_endpoint.py +0 -0
  234. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_metrics_jsonl.py +0 -0
  235. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_metrics_prometheus.py +0 -0
  236. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_metrics_prometheus_cache.py +0 -0
  237. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_models_passthrough.py +0 -0
  238. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_openai_compat.py +0 -0
  239. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_openrouter_roster_diff.py +0 -0
  240. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_output_filters.py +0 -0
  241. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_output_filters_adapters.py +0 -0
  242. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_partial_stitch.py +0 -0
  243. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_plugins_integration.py +0 -0
  244. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_plugins_loader.py +0 -0
  245. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_plugins_registry.py +0 -0
  246. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_reasoning_strip.py +0 -0
  247. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_repair_byte_fallback.py +0 -0
  248. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_request_log.py +0 -0
  249. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_role_normalization.py +0 -0
  250. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_routing_adaptive.py +0 -0
  251. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_self_healing.py +0 -0
  252. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_setup_sh.py +0 -0
  253. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_state_store.py +0 -0
  254. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_token_estimation.py +0 -0
  255. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_token_estimation_accurate.py +0 -0
  256. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_tokens_saved_metric.py +0 -0
  257. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_tool_repair.py +0 -0
  258. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_tool_repair_lenient.py +0 -0
  259. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_tool_repair_r4.py +0 -0
  260. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_toolrepair_bench.py +0 -0
  261. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_translation_anthropic.py +0 -0
  262. {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_translation_reverse.py +0 -0
@@ -9,6 +9,94 @@ are kept verbatim where the Japanese text itself is the subject).
9
9
 
10
10
  ---
11
11
 
12
+ ## [v2.7.8] — 2026-07-10 (agent_cli Phase 1d: grok)
13
+
14
+ Adds the second target to the `agent_cli` adapter: `agent: grok` (the xAI
15
+ Grok CLI) is now implemented alongside `claude` from Phase 1a. The
16
+ implementation was verified against the real CLI (grok v0.2.93, [stable]
17
+ channel, 2026-07-10), and several design-time assumptions from the 2026-07
18
+ research did not survive contact with it: the CLI now supports OAuth
19
+ subscription login (the design assumed grok was API-key-only with no
20
+ subscription path), `--sandbox` takes a profile value
21
+ (`off|workspace|read-only|strict`) rather than being a bare flag, a
22
+ Claude-Code-compatible `--permission-mode` exists, and `-p`/`--single` only
23
+ accepts the prompt as an argv value (stdin is not accepted as the prompt) —
24
+ so the adapter delivers the prompt via a temp file instead. The grok CLI is
25
+ still early beta: version pinning is recommended, and the JSON output is
26
+ parsed defensively so schema churn degrades into retryable fallback rather
27
+ than hard failure. `codex` / `gemini` remain schema-declared but rejected
28
+ (Phase 1b/1c pending).
29
+
30
+ ### Added
31
+
32
+ - **`agent: grok` builder/parser in `AgentCliAdapter`**
33
+ (`coderouter/adapters/agent_cli.py`) — invokes the Grok CLI one-shot as
34
+ `grok --prompt-file <workdir>/.coderouter-prompt-<uuid>.txt
35
+ --output-format json -m <model> --cwd <workdir> --max-turns <N>
36
+ --no-memory --sandbox read-only --permission-mode plan` (read_only
37
+ default). Auth is subscription-first, same as claude: the CLI's OAuth
38
+ login (`grok login`, SuperGrok / X Premium+) stores credentials at
39
+ `~/.grok/auth.json` (7-day expiry, auto-refresh; `GROK_HOME` overrides),
40
+ which the adapter's `HOME` inheritance reaches with
41
+ `passthrough_env: []`. For CI/API-key use the CLI reads
42
+ `GROK_CODE_XAI_API_KEY` (not `XAI_API_KEY`) — list it in
43
+ `passthrough_env` only if needed; it takes precedence over OAuth.
44
+ - **`sandbox_mode` → grok flag mapping** — `read_only` →
45
+ `--sandbox read-only --permission-mode plan`; `edit` →
46
+ `--sandbox workspace --permission-mode acceptEdits`; `full_auto` →
47
+ `--sandbox workspace --always-approve`. As with claude, the effective
48
+ mode is clamped to read-only whenever `allow_file_writes=false`,
49
+ regardless of `sandbox_mode` (defense in depth). Unlike claude,
50
+ `full_auto` maps distinctly from `edit`.
51
+ - **Prompt delivery via `--prompt-file`** — grok's `-p`/`--single` requires
52
+ the prompt as an argv VALUE (verified on the real CLI: stdin is not
53
+ accepted as the prompt), and putting huge prompts on argv would hit
54
+ Linux's `MAX_ARG_STRLEN` (~128KiB) and expose the full prompt text to
55
+ `ps`. The adapter instead writes the prompt to a mode-`0600` temp file
56
+ inside the isolated workdir and passes `--prompt-file`; the file is
57
+ always deleted afterwards, including on the timeout and error paths.
58
+ - **`--no-memory` always passed** — grok has a cross-session memory
59
+ feature; disabling it unconditionally preserves the
60
+ "one request = one stateless transformation" ethos (a previous call's
61
+ memory can never leak into the next response). Not configurable.
62
+ - **grok JSON parsing + zero-usage reporting** — `--output-format json`
63
+ emits a single JSON object `{"text", "stopReason", "sessionId",
64
+ "requestId", "thought"?}` (verified on grok v0.2.93). `text` becomes the
65
+ final answer; `sessionId` surfaces as the `coderouter_session_id`
66
+ response metadata. There are NO token-usage or cost fields, so usage is
67
+ reported as zeros and `coderouter_cost_usd` stays 0 unless the operator
68
+ sets unit prices in `ProviderConfig.cost` (in contrast to claude's
69
+ direct `total_cost_usd`). Parsing is defensive: anything malformed
70
+ raises a retryable `AdapterError` and the fallback chain advances.
71
+ Runtime errors (exit 1 with the error text on stderr) surface a stderr
72
+ tail in the `AdapterError` message.
73
+
74
+ ### Changed
75
+
76
+ - **Unsupported-agent error message** — constructing the adapter with a
77
+ not-yet-implemented agent now lists what IS implemented (e.g.
78
+ `agent 'codex' is not implemented yet (implemented: claude, grok)`)
79
+ instead of the Phase-1a-specific "claude only" wording. Still
80
+ `retryable=False` — a configuration error, not a fallback candidate.
81
+ - **Schema docstrings** (`coderouter/config/schemas.py`) — `AgentCliConfig`
82
+ field descriptions updated for the two-agent reality; the design-time
83
+ "grok requires `XAI_API_KEY` in `passthrough_env`" validation note was
84
+ dropped as obsolete (grok now supports subscription OAuth).
85
+ - **Docs & examples** — `docs/backends/external-agents.md`/`.en.md` gain a
86
+ full grok section (config example, adapter argv, sandbox mapping table,
87
+ prompt-file rationale, `--no-memory` rationale, JSON schema and
88
+ zero-usage note, OAuth setup steps, early-beta/version-pin caveat) and
89
+ updated implementation-status wording. The design doc
90
+ `docs/designs/external-agents-adapter.md` gets a new appendix §11.3
91
+ recording the verified deltas between the 2026-07 research and grok
92
+ v0.2.93 (body text left as the historical record).
93
+ `examples/providers-agent-cli.yaml` promotes grok out of the
94
+ not-implemented preview into a documented (still commented-out, so
95
+ copying the file doesn't require grok to be installed) block with the
96
+ corrected model (`grok-4.5` — `grok-code-fast-1` was retired
97
+ 2026-05-15), `paid: false` for subscription OAuth, and
98
+ `passthrough_env: []`.
99
+
12
100
  ## [v2.7.7] — 2026-07-10 (External coding agents: agent_cli adapter, Phase 1a claude)
13
101
 
14
102
  **PR #62 / #63**. Adds a new adapter kind, `agent_cli`, that lets CodeRouter front an external
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coderouter-cli
3
- Version: 2.7.7
3
+ Version: 2.7.8
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
@@ -18,24 +18,36 @@ CodeRouter merely performs the one conversion. Following the
18
18
  ``openai_compat`` precedent, a *single* adapter class fronts *multiple*
19
19
  target agents, dispatched on the ``agent`` field.
20
20
 
21
- Phase 1a scope
22
- ==============
23
-
24
- Only the ``claude`` (Claude Code CLI) target is implemented here — it is
25
- the most stable CLI, has the most fine-grained safety controls, and is the
26
- only one that emits ``total_cost_usd`` directly (making it the reference
27
- implementation for the parser / cost path). The other agents
28
- (codex / gemini / grok) are declared in the config schema so providers.yaml
29
- is forward-compatible, but constructing an adapter for them raises a clear
30
- ``AdapterError`` until their phase lands.
21
+ Implemented agents (Phase 1a + 1d)
22
+ ==================================
23
+
24
+ Two targets are implemented here:
25
+
26
+ * ``claude`` (Claude Code CLI, Phase 1a) the most stable CLI, the most
27
+ fine-grained safety controls, and the only one that emits
28
+ ``total_cost_usd`` directly (making it the reference implementation for
29
+ the parser / cost path).
30
+ * ``grok`` (grok CLI, Phase 1d) — headless one-shot via ``--prompt-file`` +
31
+ ``--output-format json``. The CLI emits no token/cost figures, so usage
32
+ is reported as zeros (cost stays 0 unless the operator sets
33
+ ``ProviderConfig.cost``, design §5.1.6). ``--no-memory`` is always passed
34
+ so a user-level cross-session memory setting cannot leak state between
35
+ requests.
36
+
37
+ The remaining agents (codex / gemini) are declared in the config schema so
38
+ providers.yaml is forward-compatible, but constructing an adapter for them
39
+ raises a clear ``AdapterError`` until their phase (1b / 1c) lands.
31
40
 
32
41
  Security (design §6, non-negotiable)
33
42
  ====================================
34
43
 
35
44
  * **allowlist argv only** — the child is launched with
36
45
  :func:`asyncio.create_subprocess_exec` and a list argv. ``shell=True`` is
37
- never used; the prompt is fed on stdin, so it is never subject to shell
38
- interpretation.
46
+ never used; the prompt is never subject to shell interpretation (claude
47
+ reads it from stdin; grok reads it from a private ``0600`` prompt file
48
+ inside the resolved workdir — its ``-p`` requires the prompt as an argv
49
+ value, and argv would both hit Linux's ~128KiB ``MAX_ARG_STRLEN`` on huge
50
+ prompts and leak the text into ``ps`` output).
39
51
  * **default read-only** — ``allow_file_writes=False`` /
40
52
  ``sandbox_mode="read_only"`` are the defaults, mapped to claude's
41
53
  ``--permission-mode plan``. Writes require explicit opt-in and the sandbox
@@ -104,6 +116,17 @@ _CLAUDE_PERMISSION_MODE = {
104
116
  "full_auto": "acceptEdits",
105
117
  }
106
118
 
119
+ # sandbox_mode → grok sandbox/approval flags (design §5.4, grok CLI v0.2.93).
120
+ # grok's ``--sandbox`` takes a built-in profile VALUE (off|workspace|
121
+ # read-only|strict) and its ``--permission-mode`` shares Claude Code's value
122
+ # set; ``full_auto`` swaps the permission mode for ``--always-approve``
123
+ # (auto-approve all tool executions).
124
+ _GROK_SANDBOX_ARGS = {
125
+ "read_only": ["--sandbox", "read-only", "--permission-mode", "plan"],
126
+ "edit": ["--sandbox", "workspace", "--permission-mode", "acceptEdits"],
127
+ "full_auto": ["--sandbox", "workspace", "--always-approve"],
128
+ }
129
+
107
130
 
108
131
  def _chunk_text(text: str, size: int = _STREAM_CHUNK_CHARS) -> Iterator[str]:
109
132
  """Split ``text`` into ``size``-char pieces for the pseudo-stream."""
@@ -112,7 +135,7 @@ def _chunk_text(text: str, size: int = _STREAM_CHUNK_CHARS) -> Iterator[str]:
112
135
 
113
136
 
114
137
  class AgentCliAdapter(BaseAdapter):
115
- """Invoke an external coding-agent CLI one-shot (Phase 1a: claude only).
138
+ """Invoke an external coding-agent CLI one-shot (claude + grok).
116
139
 
117
140
  The ``agent`` field selects the argv builder / output parser via the
118
141
  dispatch tables built in :meth:`__init__`, mirroring how
@@ -122,9 +145,10 @@ class AgentCliAdapter(BaseAdapter):
122
145
  def __init__(self, config: ProviderConfig) -> None:
123
146
  """Bind to a ``ProviderConfig`` and reject unsupported agents.
124
147
 
125
- Constructing an adapter for an agent other than ``claude`` raises a
126
- non-retryable :class:`AdapterError` — the other targets are declared
127
- in the schema but not implemented until their phase (design §9).
148
+ Constructing an adapter for an agent other than ``claude`` / ``grok``
149
+ raises a non-retryable :class:`AdapterError` — the other targets are
150
+ declared in the schema but not implemented until their phase
151
+ (design §9).
128
152
  """
129
153
  super().__init__(config)
130
154
  if config.agent_cli is None: # pragma: no cover - schema enforces this
@@ -134,21 +158,30 @@ class AgentCliAdapter(BaseAdapter):
134
158
  retryable=False,
135
159
  )
136
160
  self.acfg: AgentCliConfig = config.agent_cli
137
- if self.acfg.agent != "claude":
161
+ if self.acfg.agent not in ("claude", "grok"):
138
162
  raise AdapterError(
139
- f"agent {self.acfg.agent!r} is not implemented in Phase 1a "
140
- f"(claude only). Configure agent='claude' or wait for the "
141
- f"agent's phase.",
163
+ f"agent {self.acfg.agent!r} is not implemented yet "
164
+ f"(implemented: claude, grok). Wait for Phase 1b/1c.",
142
165
  provider=config.name,
143
166
  retryable=False,
144
167
  )
145
- # agent → argv builder / output parser dispatch tables. Phase 1a
146
- # registers only claude; later phases add codex / gemini / grok.
147
- self._builders = {"claude": self._build_claude_argv}
148
- self._parsers = {"claude": self._parse_claude}
149
- # claude reads its print-mode prompt from stdin (10MB cap), which
150
- # keeps argv free of the (potentially huge) prompt text.
151
- self._uses_stdin = True
168
+ # agent → argv builder / output parser dispatch tables. claude landed
169
+ # in Phase 1a, grok in Phase 1d; Phase 1b/1c add codex / gemini.
170
+ self._builders = {
171
+ "claude": self._build_claude_argv,
172
+ "grok": self._build_grok_argv,
173
+ }
174
+ self._parsers = {
175
+ "claude": self._parse_claude,
176
+ "grok": self._parse_grok,
177
+ }
178
+ # Prompt delivery is per-agent: claude reads its print-mode prompt
179
+ # from stdin (10MB cap), which keeps argv free of the (potentially
180
+ # huge) prompt text. grok's ``-p`` REQUIRES the prompt as its argv
181
+ # value (piped stdin is only appended as extra context, verified on
182
+ # v0.2.93), so grok gets the prompt via ``--prompt-file`` instead —
183
+ # see ``_write_prompt_file`` for the rationale.
184
+ self._uses_stdin = self.acfg.agent == "claude"
152
185
 
153
186
  # ------------------------------------------------------------------
154
187
  # BaseAdapter contract
@@ -177,8 +210,7 @@ class AgentCliAdapter(BaseAdapter):
177
210
  depth = self._current_depth()
178
211
  if depth >= self.acfg.agent_depth_limit:
179
212
  raise AdapterError(
180
- f"agent recursion depth {depth} >= limit "
181
- f"{self.acfg.agent_depth_limit}",
213
+ f"agent recursion depth {depth} >= limit {self.acfg.agent_depth_limit}",
182
214
  provider=self.name,
183
215
  retryable=False,
184
216
  )
@@ -195,68 +227,82 @@ class AgentCliAdapter(BaseAdapter):
195
227
 
196
228
  prompt = self._render_prompt(request, overrides)
197
229
  workdir = self._resolve_workdir()
198
- argv = self._builders[self.acfg.agent](workdir)
199
- # Resolve the executable to an absolute path so argv[0] is a concrete
200
- # binary independent of the child's minimal PATH (design §6 allowlist).
201
- resolved = shutil.which(argv[0])
202
- if resolved is not None:
203
- argv = [resolved, *argv[1:]]
204
- env = self._build_child_env()
205
-
206
- logger.info(
207
- "agent-cli-exec",
208
- extra={
209
- "provider": self.name,
210
- "agent": self.acfg.agent,
211
- "argv0": argv[0],
212
- "timeout_s": timeout,
213
- },
214
- )
215
230
 
231
+ # Agents that cannot take the prompt on stdin (grok) get it through a
232
+ # private temp file inside the workdir; it is ALWAYS removed in the
233
+ # ``finally`` below, including on timeout / exception paths.
234
+ prompt_file = None if self._uses_stdin else self._write_prompt_file(prompt, workdir)
216
235
  try:
217
- proc = await asyncio.create_subprocess_exec(
218
- *argv,
219
- stdin=asyncio.subprocess.PIPE,
220
- stdout=asyncio.subprocess.PIPE,
221
- stderr=asyncio.subprocess.PIPE,
222
- cwd=workdir,
223
- env=env,
224
- # New session/process group so a timeout can SIGKILL the whole
225
- # group (the CLI hangs its real LLM call off a child).
226
- start_new_session=True,
236
+ argv = self._builders[self.acfg.agent](workdir, prompt_file)
237
+ # Resolve the executable to an absolute path so argv[0] is a
238
+ # concrete binary independent of the child's minimal PATH
239
+ # (design §6 allowlist).
240
+ resolved = shutil.which(argv[0])
241
+ if resolved is not None:
242
+ argv = [resolved, *argv[1:]]
243
+ env = self._build_child_env()
244
+
245
+ logger.info(
246
+ "agent-cli-exec",
247
+ extra={
248
+ "provider": self.name,
249
+ "agent": self.acfg.agent,
250
+ "argv0": argv[0],
251
+ "timeout_s": timeout,
252
+ },
227
253
  )
228
- except (FileNotFoundError, OSError) as exc:
229
- raise AdapterError(
230
- f"failed to launch {self.acfg.command!r}: {exc}",
231
- provider=self.name,
232
- retryable=False,
233
- ) from exc
234
254
 
235
- stdin_bytes = prompt.encode("utf-8") if self._uses_stdin else None
236
- try:
237
- stdout, stderr = await asyncio.wait_for(
238
- proc.communicate(input=stdin_bytes), timeout=timeout
239
- )
240
- except TimeoutError as exc:
241
- self._kill_process_group(proc)
242
- with contextlib.suppress(Exception):
243
- await proc.wait()
244
- raise AdapterError(
245
- f"{self.acfg.agent} exec timed out after {timeout}s",
246
- provider=self.name,
247
- retryable=True,
248
- ) from exc
255
+ try:
256
+ proc = await asyncio.create_subprocess_exec(
257
+ *argv,
258
+ stdin=asyncio.subprocess.PIPE,
259
+ stdout=asyncio.subprocess.PIPE,
260
+ stderr=asyncio.subprocess.PIPE,
261
+ cwd=workdir,
262
+ env=env,
263
+ # New session/process group so a timeout can SIGKILL the
264
+ # whole group (the CLI hangs its real LLM call off a
265
+ # child).
266
+ start_new_session=True,
267
+ )
268
+ except (FileNotFoundError, OSError) as exc:
269
+ raise AdapterError(
270
+ f"failed to launch {self.acfg.command!r}: {exc}",
271
+ provider=self.name,
272
+ retryable=False,
273
+ ) from exc
249
274
 
250
- if proc.returncode != 0:
251
- detail = self._error_detail(stdout, stderr)
252
- raise AdapterError(
253
- f"{self.acfg.agent} exited {proc.returncode}: {detail}",
254
- provider=self.name,
255
- status_code=None,
256
- retryable=self._is_retryable_exit(proc.returncode),
257
- )
275
+ stdin_bytes = prompt.encode("utf-8") if self._uses_stdin else None
276
+ try:
277
+ stdout, stderr = await asyncio.wait_for(
278
+ proc.communicate(input=stdin_bytes), timeout=timeout
279
+ )
280
+ except TimeoutError as exc:
281
+ self._kill_process_group(proc)
282
+ with contextlib.suppress(Exception):
283
+ await proc.wait()
284
+ raise AdapterError(
285
+ f"{self.acfg.agent} exec timed out after {timeout}s",
286
+ provider=self.name,
287
+ retryable=True,
288
+ ) from exc
258
289
 
259
- final_text, usage, meta = self._parsers[self.acfg.agent](stdout, stderr)
290
+ if proc.returncode != 0:
291
+ detail = self._error_detail(stdout, stderr)
292
+ raise AdapterError(
293
+ f"{self.acfg.agent} exited {proc.returncode}: {detail}",
294
+ provider=self.name,
295
+ status_code=None,
296
+ retryable=self._is_retryable_exit(proc.returncode),
297
+ )
298
+
299
+ final_text, usage, meta = self._parsers[self.acfg.agent](stdout, stderr)
300
+ finally:
301
+ if prompt_file is not None:
302
+ # Best-effort cleanup — the child may already have exited and
303
+ # a vanished file is not an error worth surfacing.
304
+ with contextlib.suppress(OSError):
305
+ os.unlink(prompt_file)
260
306
  return self._to_chat_response(final_text, usage, meta)
261
307
 
262
308
  async def stream(
@@ -281,9 +327,7 @@ class AgentCliAdapter(BaseAdapter):
281
327
  id=resp.id,
282
328
  created=resp.created,
283
329
  model=resp.model,
284
- choices=[
285
- {"index": 0, "delta": {"content": piece}, "finish_reason": None}
286
- ],
330
+ choices=[{"index": 0, "delta": {"content": piece}, "finish_reason": None}],
287
331
  )
288
332
  yield StreamChunk(
289
333
  id=resp.id,
@@ -297,7 +341,7 @@ class AgentCliAdapter(BaseAdapter):
297
341
  # claude argv builder + output parser
298
342
  # ------------------------------------------------------------------
299
343
 
300
- def _build_claude_argv(self, workdir: str) -> list[str]:
344
+ def _build_claude_argv(self, workdir: str, prompt_file: str | None = None) -> list[str]:
301
345
  """Assemble the ``claude -p`` argv (design §5.1.5 / §5.4).
302
346
 
303
347
  Shape::
@@ -305,10 +349,13 @@ class AgentCliAdapter(BaseAdapter):
305
349
  claude -p --output-format json --model <m> --max-turns <n>
306
350
  --permission-mode <plan|acceptEdits> --add-dir <workdir>
307
351
 
308
- The prompt is fed on stdin (not argv), so it never appears here.
309
- ``--bare`` is deliberately NOT added it would skip OAuth/keychain
310
- reads and break subscription auth (design §5.3.4).
352
+ The prompt is fed on stdin (not argv), so it never appears here and
353
+ ``prompt_file`` is ignored (it exists only to keep the builder
354
+ signature uniform across agents). ``--bare`` is deliberately NOT
355
+ added — it would skip OAuth/keychain reads and break subscription
356
+ auth (design §5.3.4).
311
357
  """
358
+ del prompt_file # claude takes the prompt on stdin, not from a file.
312
359
  model = self.acfg.model or self.config.model
313
360
  argv = [self.acfg.command, "-p", "--output-format", "json", "--model", model]
314
361
  if self.acfg.max_turns is not None:
@@ -418,10 +465,154 @@ class AgentCliAdapter(BaseAdapter):
418
465
  usage["duration_ms"] = data["duration_ms"]
419
466
  return usage
420
467
 
468
+ # ------------------------------------------------------------------
469
+ # grok argv builder + output parser (Phase 1d)
470
+ # ------------------------------------------------------------------
471
+
472
+ def _build_grok_argv(self, workdir: str, prompt_file: str | None = None) -> list[str]:
473
+ """Assemble the grok headless argv (design §5, grok CLI v0.2.93).
474
+
475
+ Shape::
476
+
477
+ grok --prompt-file <f> --output-format json -m <m> --cwd <w>
478
+ --max-turns <n> --no-memory --sandbox <profile>
479
+ [--permission-mode <mode> | --always-approve]
480
+
481
+ The prompt travels via ``--prompt-file`` (never argv / stdin): grok's
482
+ ``-p`` requires the prompt as its argv value, and putting it there
483
+ would hit Linux's ~128KiB ``MAX_ARG_STRLEN`` on large prompts and
484
+ leak the text into ``ps`` output. ``--no-memory`` is deliberate: it
485
+ enforces the one-request-one-transformation statelessness even if
486
+ the user's grok config enables cross-session memory.
487
+ """
488
+ if prompt_file is None: # pragma: no cover - generate() always supplies it
489
+ raise AdapterError(
490
+ "grok argv requires a prompt file",
491
+ provider=self.name,
492
+ retryable=False,
493
+ )
494
+ model = self.acfg.model or self.config.model
495
+ argv = [
496
+ self.acfg.command,
497
+ "--prompt-file",
498
+ prompt_file,
499
+ "--output-format",
500
+ "json",
501
+ "-m",
502
+ model,
503
+ "--cwd",
504
+ workdir,
505
+ ]
506
+ if self.acfg.max_turns is not None:
507
+ argv += ["--max-turns", str(self.acfg.max_turns)]
508
+ argv += ["--no-memory"]
509
+ argv += self._grok_sandbox_args()
510
+ return argv
511
+
512
+ def _grok_sandbox_args(self) -> list[str]:
513
+ """Map ``sandbox_mode`` → grok sandbox/approval flags, clamped.
514
+
515
+ Same clamp as claude (design §5.4): when ``allow_file_writes`` is
516
+ False the effective mode is forced to ``read_only`` regardless of
517
+ ``sandbox_mode``, so writes always require the explicit opt-in.
518
+ """
519
+ mode = self.acfg.sandbox_mode if self.acfg.allow_file_writes else "read_only"
520
+ return list(_GROK_SANDBOX_ARGS[mode])
521
+
522
+ def _parse_grok(
523
+ self, stdout: bytes, stderr: bytes
524
+ ) -> tuple[str, dict[str, Any], dict[str, Any]]:
525
+ """Parse grok ``--output-format json`` output (verified v0.2.93).
526
+
527
+ Real-run shape::
528
+
529
+ {"text": "...", "stopReason": "EndTurn", "sessionId": "<uuid>",
530
+ "requestId": "<uuid>", "thought": "..."}
531
+
532
+ The CLI is early beta, so parsing is deliberately defensive: empty /
533
+ non-JSON / non-object stdout and a missing ``text`` field all raise
534
+ a retryable :class:`AdapterError` so the chain can fall through.
535
+ ``thought`` is ignored — only the final ``text`` is the answer.
536
+ """
537
+ text = stdout.decode("utf-8", "replace").strip()
538
+ if not text:
539
+ raise AdapterError(
540
+ "grok produced no stdout to parse",
541
+ provider=self.name,
542
+ retryable=True,
543
+ )
544
+ try:
545
+ data = json.loads(text)
546
+ except json.JSONDecodeError as exc:
547
+ raise AdapterError(
548
+ f"grok emitted non-JSON output: {exc}",
549
+ provider=self.name,
550
+ retryable=True,
551
+ ) from exc
552
+ if not isinstance(data, dict):
553
+ raise AdapterError(
554
+ "grok JSON output was not an object",
555
+ provider=self.name,
556
+ retryable=True,
557
+ )
558
+ result = data.get("text")
559
+ if not isinstance(result, str):
560
+ raise AdapterError(
561
+ "grok JSON output missing string 'text' field",
562
+ provider=self.name,
563
+ retryable=True,
564
+ )
565
+
566
+ # grok emits NO token usage / cost fields (verified), so usage is
567
+ # all-zeros; the cost dashboard shows 0 for this provider unless the
568
+ # operator sets ``ProviderConfig.cost`` rates (design §5.1.6).
569
+ usage: dict[str, Any] = {
570
+ "prompt_tokens": 0,
571
+ "completion_tokens": 0,
572
+ "total_tokens": 0,
573
+ }
574
+ meta: dict[str, Any] = {}
575
+ session_id = data.get("sessionId")
576
+ if isinstance(session_id, str):
577
+ meta["coderouter_session_id"] = session_id
578
+ return result, usage, meta
579
+
421
580
  # ------------------------------------------------------------------
422
581
  # helpers: prompt rendering, response shaping, env, workdir, kill
423
582
  # ------------------------------------------------------------------
424
583
 
584
+ def _write_prompt_file(self, prompt: str, workdir: str) -> str:
585
+ """Write the prompt to a private ``0600`` temp file in ``workdir``.
586
+
587
+ Rationale: grok's ``-p`` requires the prompt as an argv value, but a
588
+ huge prompt would hit Linux's ~128KiB ``MAX_ARG_STRLEN`` and argv
589
+ leaks into ``ps`` output; file delivery keeps argv small and private,
590
+ and ``0600`` + the isolated workdir bounds exposure. ``O_EXCL`` with
591
+ a uuid4 name makes creation race-free; the caller (``generate``)
592
+ deletes the file in a ``finally`` block on every path.
593
+ """
594
+ path = os.path.join(workdir, f".coderouter-prompt-{uuid.uuid4().hex}.txt")
595
+ try:
596
+ fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
597
+ except OSError as exc:
598
+ raise AdapterError(
599
+ f"failed to create prompt file in {workdir}: {exc}",
600
+ provider=self.name,
601
+ retryable=False,
602
+ ) from exc
603
+ try:
604
+ with os.fdopen(fd, "w", encoding="utf-8") as handle:
605
+ handle.write(prompt)
606
+ except OSError as exc:
607
+ with contextlib.suppress(OSError):
608
+ os.unlink(path)
609
+ raise AdapterError(
610
+ f"failed to write prompt file {path}: {exc}",
611
+ provider=self.name,
612
+ retryable=False,
613
+ ) from exc
614
+ return path
615
+
425
616
  def _to_chat_response(
426
617
  self, final_text: str, usage: dict[str, Any], meta: dict[str, Any]
427
618
  ) -> ChatResponse:
@@ -442,9 +633,7 @@ class AgentCliAdapter(BaseAdapter):
442
633
  **meta,
443
634
  )
444
635
 
445
- def _render_prompt(
446
- self, request: ChatRequest, overrides: ProviderCallOverrides | None
447
- ) -> str:
636
+ def _render_prompt(self, request: ChatRequest, overrides: ProviderCallOverrides | None) -> str:
448
637
  """Flatten the chat messages into a single role-tagged prompt string.
449
638
 
450
639
  The profile-level ``append_system_prompt`` (if any) is prepended as a