code-context-control 2.32.2__tar.gz → 2.34.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 (203) hide show
  1. {code_context_control-2.32.2 → code_context_control-2.34.0}/PKG-INFO +2 -2
  2. {code_context_control-2.32.2 → code_context_control-2.34.0}/README.md +1 -1
  3. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/c3.py +21 -4
  4. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/hook_ghost_files.py +12 -2
  5. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/hub_server.py +31 -107
  6. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/mcp_server.py +3 -1
  7. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/server.py +31 -114
  8. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/tools/filter.py +2 -2
  9. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/tools/read.py +37 -0
  10. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/tools/shell.py +81 -5
  11. {code_context_control-2.32.2 → code_context_control-2.34.0}/code_context_control.egg-info/PKG-INFO +2 -2
  12. {code_context_control-2.32.2 → code_context_control-2.34.0}/code_context_control.egg-info/SOURCES.txt +7 -0
  13. code_context_control-2.34.0/core/mcp_toml.py +128 -0
  14. code_context_control-2.34.0/core/web_security.py +174 -0
  15. {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/mcp_oracle.py +56 -11
  16. {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/oracle_server.py +16 -6
  17. {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/services/tool_registry.py +17 -2
  18. {code_context_control-2.32.2 → code_context_control-2.34.0}/pyproject.toml +1 -1
  19. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/claude_md.py +1 -1
  20. {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_c3_shell.py +34 -0
  21. code_context_control-2.34.0/tests/test_mcp_host_guard.py +52 -0
  22. code_context_control-2.34.0/tests/test_mcp_toml.py +97 -0
  23. code_context_control-2.34.0/tests/test_read_coercion.py +68 -0
  24. code_context_control-2.34.0/tests/test_shell_robustness.py +90 -0
  25. code_context_control-2.34.0/tests/test_web_security.py +106 -0
  26. {code_context_control-2.32.2 → code_context_control-2.34.0}/LICENSE +0 -0
  27. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/__init__.py +0 -0
  28. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/_hook_utils.py +0 -0
  29. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/commands/__init__.py +0 -0
  30. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/commands/common.py +0 -0
  31. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/commands/parser.py +0 -0
  32. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/docs.html +0 -0
  33. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/edits.html +0 -0
  34. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/hook_auto_snapshot.py +0 -0
  35. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/hook_c3_signal.py +0 -0
  36. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/hook_c3read.py +0 -0
  37. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/hook_edit_ledger.py +0 -0
  38. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/hook_edit_unlock.py +0 -0
  39. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/hook_filter.py +0 -0
  40. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/hook_pretool_enforce.py +0 -0
  41. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/hook_read.py +0 -0
  42. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/hook_session_stats.py +0 -0
  43. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/hook_terse_advisor.py +0 -0
  44. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/hub.html +0 -0
  45. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/mcp_proxy.py +0 -0
  46. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/tools/__init__.py +0 -0
  47. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/tools/_helpers.py +0 -0
  48. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/tools/agent.py +0 -0
  49. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/tools/bitbucket.py +0 -0
  50. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/tools/compress.py +0 -0
  51. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/tools/delegate.py +0 -0
  52. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/tools/edit.py +0 -0
  53. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/tools/edits.py +0 -0
  54. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/tools/impact.py +0 -0
  55. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/tools/memory.py +0 -0
  56. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/tools/project.py +0 -0
  57. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/tools/search.py +0 -0
  58. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/tools/session.py +0 -0
  59. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/tools/status.py +0 -0
  60. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/tools/validate.py +0 -0
  61. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/ui/api.js +0 -0
  62. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/ui/app.js +0 -0
  63. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/ui/components/bitbucket.js +0 -0
  64. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/ui/components/chat.js +0 -0
  65. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/ui/components/dashboard.js +0 -0
  66. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/ui/components/edits.js +0 -0
  67. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/ui/components/instructions.js +0 -0
  68. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/ui/components/memory.js +0 -0
  69. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/ui/components/sessions.js +0 -0
  70. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/ui/components/settings.js +0 -0
  71. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/ui/components/sidebar.js +0 -0
  72. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/ui/icons.js +0 -0
  73. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/ui/shared.js +0 -0
  74. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/ui/theme.js +0 -0
  75. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/ui.html +0 -0
  76. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/ui_legacy.html +0 -0
  77. {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/ui_nano.html +0 -0
  78. {code_context_control-2.32.2 → code_context_control-2.34.0}/code_context_control.egg-info/dependency_links.txt +0 -0
  79. {code_context_control-2.32.2 → code_context_control-2.34.0}/code_context_control.egg-info/entry_points.txt +0 -0
  80. {code_context_control-2.32.2 → code_context_control-2.34.0}/code_context_control.egg-info/requires.txt +0 -0
  81. {code_context_control-2.32.2 → code_context_control-2.34.0}/code_context_control.egg-info/top_level.txt +0 -0
  82. {code_context_control-2.32.2 → code_context_control-2.34.0}/core/__init__.py +0 -0
  83. {code_context_control-2.32.2 → code_context_control-2.34.0}/core/config.py +0 -0
  84. {code_context_control-2.32.2 → code_context_control-2.34.0}/core/ide.py +0 -0
  85. {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/__init__.py +0 -0
  86. {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/config.py +0 -0
  87. {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/oracle.html +0 -0
  88. {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/services/__init__.py +0 -0
  89. {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/services/api_auth.py +0 -0
  90. {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/services/c3_bridge.py +0 -0
  91. {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/services/chat_engine.py +0 -0
  92. {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/services/chat_store.py +0 -0
  93. {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/services/cross_memory.py +0 -0
  94. {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/services/federated_graph.py +0 -0
  95. {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/services/health_checker.py +0 -0
  96. {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/services/insight_engine.py +0 -0
  97. {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/services/memory_reader.py +0 -0
  98. {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/services/memory_writer.py +0 -0
  99. {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/services/ollama_bridge.py +0 -0
  100. {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/services/project_scanner.py +0 -0
  101. {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/services/review_agent.py +0 -0
  102. {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/services/tool_executor.py +0 -0
  103. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/__init__.py +0 -0
  104. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/activity_log.py +0 -0
  105. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/agent_base.py +0 -0
  106. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/agents.py +0 -0
  107. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/auto_memory.py +0 -0
  108. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/bench/__init__.py +0 -0
  109. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/bench/external/__init__.py +0 -0
  110. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/bench/external/aider_polyglot.py +0 -0
  111. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/bench/external/swe_bench.py +0 -0
  112. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/benchmark_dashboard.py +0 -0
  113. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/bitbucket_client.py +0 -0
  114. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/bitbucket_credentials.py +0 -0
  115. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/compressor.py +0 -0
  116. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/context_snapshot.py +0 -0
  117. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/conversation_store.py +0 -0
  118. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/doc_index.py +0 -0
  119. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/e2e_benchmark.py +0 -0
  120. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/e2e_evaluator.py +0 -0
  121. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/e2e_tasks.py +0 -0
  122. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/edit_ledger.py +0 -0
  123. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/embedding_index.py +0 -0
  124. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/error_reporting.py +0 -0
  125. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/file_memory.py +0 -0
  126. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/hub_service.py +0 -0
  127. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/indexer.py +0 -0
  128. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/memory.py +0 -0
  129. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/memory_consolidator.py +0 -0
  130. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/memory_graph.py +0 -0
  131. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/memory_grounder.py +0 -0
  132. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/memory_scorer.py +0 -0
  133. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/metrics.py +0 -0
  134. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/notifications.py +0 -0
  135. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/ollama_client.py +0 -0
  136. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/output_filter.py +0 -0
  137. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/parser.py +0 -0
  138. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/project_manager.py +0 -0
  139. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/project_runtime.py +0 -0
  140. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/protocol.py +0 -0
  141. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/proxy_state.py +0 -0
  142. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/retrieval_broker.py +0 -0
  143. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/router.py +0 -0
  144. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/runtime.py +0 -0
  145. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/session_benchmark.py +0 -0
  146. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/session_manager.py +0 -0
  147. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/session_preloader.py +0 -0
  148. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/text_index.py +0 -0
  149. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/tool_classifier.py +0 -0
  150. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/transcript_index.py +0 -0
  151. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/validation_cache.py +0 -0
  152. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/vector_store.py +0 -0
  153. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/version_tracker.py +0 -0
  154. {code_context_control-2.32.2 → code_context_control-2.34.0}/services/watcher.py +0 -0
  155. {code_context_control-2.32.2 → code_context_control-2.34.0}/setup.cfg +0 -0
  156. {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_aider_polyglot.py +0 -0
  157. {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_bitbucket_cli_smoke.py +0 -0
  158. {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_bitbucket_client.py +0 -0
  159. {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_bitbucket_credentials.py +0 -0
  160. {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_bitbucket_tool.py +0 -0
  161. {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_cli_smoke.py +0 -0
  162. {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_e2e_benchmark.py +0 -0
  163. {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_edit_normalization.py +0 -0
  164. {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_enforcement_flip.py +0 -0
  165. {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_federated_graph.py +0 -0
  166. {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_ghost_files.py +0 -0
  167. {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_hub_server_smoke.py +0 -0
  168. {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_mcp_server_smoke.py +0 -0
  169. {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_memory_graph_api.py +0 -0
  170. {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_memory_system.py +0 -0
  171. {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_notification_discipline.py +0 -0
  172. {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_oracle_api_auth.py +0 -0
  173. {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_oracle_apikey_api.py +0 -0
  174. {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_oracle_discovery_api.py +0 -0
  175. {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_output_filter.py +0 -0
  176. {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_permissions.py +0 -0
  177. {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_project_manager.py +0 -0
  178. {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_project_manager_merge.py +0 -0
  179. {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_project_tool.py +0 -0
  180. {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_session_benchmark.py +0 -0
  181. {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_session_budget.py +0 -0
  182. {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_swe_bench.py +0 -0
  183. {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_tool_registry.py +0 -0
  184. {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_validate.py +0 -0
  185. {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_windows_reliability.py +0 -0
  186. {code_context_control-2.32.2 → code_context_control-2.34.0}/tui/__init__.py +0 -0
  187. {code_context_control-2.32.2 → code_context_control-2.34.0}/tui/backend.py +0 -0
  188. {code_context_control-2.32.2 → code_context_control-2.34.0}/tui/main.py +0 -0
  189. {code_context_control-2.32.2 → code_context_control-2.34.0}/tui/screens/__init__.py +0 -0
  190. {code_context_control-2.32.2 → code_context_control-2.34.0}/tui/screens/benchmark_view.py +0 -0
  191. {code_context_control-2.32.2 → code_context_control-2.34.0}/tui/screens/claudemd_view.py +0 -0
  192. {code_context_control-2.32.2 → code_context_control-2.34.0}/tui/screens/compress_view.py +0 -0
  193. {code_context_control-2.32.2 → code_context_control-2.34.0}/tui/screens/index_view.py +0 -0
  194. {code_context_control-2.32.2 → code_context_control-2.34.0}/tui/screens/init_view.py +0 -0
  195. {code_context_control-2.32.2 → code_context_control-2.34.0}/tui/screens/mcp_view.py +0 -0
  196. {code_context_control-2.32.2 → code_context_control-2.34.0}/tui/screens/optimize_view.py +0 -0
  197. {code_context_control-2.32.2 → code_context_control-2.34.0}/tui/screens/pipe_view.py +0 -0
  198. {code_context_control-2.32.2 → code_context_control-2.34.0}/tui/screens/projects_view.py +0 -0
  199. {code_context_control-2.32.2 → code_context_control-2.34.0}/tui/screens/search_view.py +0 -0
  200. {code_context_control-2.32.2 → code_context_control-2.34.0}/tui/screens/session_view.py +0 -0
  201. {code_context_control-2.32.2 → code_context_control-2.34.0}/tui/screens/stats.py +0 -0
  202. {code_context_control-2.32.2 → code_context_control-2.34.0}/tui/screens/ui_view.py +0 -0
  203. {code_context_control-2.32.2 → code_context_control-2.34.0}/tui/theme.tcss +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-context-control
3
- Version: 2.32.2
3
+ Version: 2.34.0
4
4
  Summary: Local code-intelligence layer for AI coding tools (Claude Code, Codex, Gemini, Copilot). Retrieve less, read less, edit safer.
5
5
  Author-email: Dimitri Tselenchuk <dtselenc@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -363,7 +363,7 @@ Real-world A/B tests: same task, with and without C3 mounted. Reports include to
363
363
 
364
364
  ## Security & privacy
365
365
 
366
- - **Hub binds to `127.0.0.1` by default.** Setting `host` to a non-loopback interface in `~/.c3/hub_config.json` is opt-in and warned at startup. **Do not expose the Hub to a public network without auth/TLS in front of it.**
366
+ - **All web servers (Hub, per-project UI, Oracle) bind to `127.0.0.1` by default and are guarded against browser-based attacks even on loopback** — a Host-header allowlist (defeats DNS rebinding) plus an Origin/Referer check on every request (defeats cross-origin CSRF), with scoped, non-wildcard CORS. A malicious web page you visit therefore cannot drive C3's local endpoints. There is still **no user authentication**, so do not expose these servers to an untrusted network without auth/TLS in front. Binding to a non-loopback interface in `~/.c3/hub_config.json` (`host`) or Oracle's config (`bind_host`) is opt-in and warned at startup; add externally-facing hostnames/IPs to an `allowed_hosts` list there so the guard permits them. _(Cross-origin/CSRF + DNS-rebinding hardening added in v2.33.0.)_
367
367
  - **No telemetry by default.** The OSS package collects nothing. Opt-in Sentry crash reporting requires the `[telemetry]` extra plus both `SENTRY_DSN` and `C3_TELEMETRY_OPT_IN=1`. Even when enabled, request bodies, local variables, and prompts are stripped before sending.
368
368
  - **API keys** for third-party model providers are read from environment variables and never persisted by C3.
369
369
  - See [`SECURITY.md`](SECURITY.md) for the full hardening guide and disclosure policy.
@@ -301,7 +301,7 @@ Real-world A/B tests: same task, with and without C3 mounted. Reports include to
301
301
 
302
302
  ## Security & privacy
303
303
 
304
- - **Hub binds to `127.0.0.1` by default.** Setting `host` to a non-loopback interface in `~/.c3/hub_config.json` is opt-in and warned at startup. **Do not expose the Hub to a public network without auth/TLS in front of it.**
304
+ - **All web servers (Hub, per-project UI, Oracle) bind to `127.0.0.1` by default and are guarded against browser-based attacks even on loopback** — a Host-header allowlist (defeats DNS rebinding) plus an Origin/Referer check on every request (defeats cross-origin CSRF), with scoped, non-wildcard CORS. A malicious web page you visit therefore cannot drive C3's local endpoints. There is still **no user authentication**, so do not expose these servers to an untrusted network without auth/TLS in front. Binding to a non-loopback interface in `~/.c3/hub_config.json` (`host`) or Oracle's config (`bind_host`) is opt-in and warned at startup; add externally-facing hostnames/IPs to an `allowed_hosts` list there so the guard permits them. _(Cross-origin/CSRF + DNS-rebinding hardening added in v2.33.0.)_
305
305
  - **No telemetry by default.** The OSS package collects nothing. Opt-in Sentry crash reporting requires the `[telemetry]` extra plus both `SENTRY_DSN` and `C3_TELEMETRY_OPT_IN=1`. Even when enabled, request bodies, local variables, and prompts are stripped before sending.
306
306
  - **API keys** for third-party model providers are read from environment variables and never persisted by C3.
307
307
  - See [`SECURITY.md`](SECURITY.md) for the full hardening guide and disclosure policy.
@@ -85,7 +85,7 @@ console = Console() if HAS_RICH else None
85
85
  # Config
86
86
  CONFIG_DIR = ".c3"
87
87
  CONFIG_FILE = ".c3/config.json"
88
- __version__ = "2.32.2"
88
+ __version__ = "2.34.0"
89
89
 
90
90
 
91
91
  def _command_deps() -> CommandDeps:
@@ -4921,8 +4921,14 @@ def cmd_install_mcp(args):
4921
4921
  # Build hook commands using the Python executable that runs c3.
4922
4922
  # On Windows, Claude Code executes hooks via /usr/bin/bash (Git Bash), which cannot
4923
4923
  # parse Windows absolute paths containing parentheses (e.g. "(C3)"). Prefix with
4924
- # "cmd /c" so cmd.exe handles path resolution instead of bash.
4925
- _hook_prefix = "cmd /c " if sys.platform == "win32" else ""
4924
+ # cmd.exe so it handles path resolution instead of bash.
4925
+ #
4926
+ # Use "cmd.exe" WITH the extension, not bare "cmd": Git Bash does not resolve bare
4927
+ # "cmd" on PATH, so the old "cmd /c …" prefix silently failed to launch any hook
4928
+ # (verified: under bash, "cmd.exe /c '<py>' '<hook>'" runs and writes the signal
4929
+ # file; "cmd /c …" returns "cmd: command not found"). The single-quoted paths are
4930
+ # correct — bash strips them and re-quotes for cmd.exe, preserving spaces/parens.
4931
+ _hook_prefix = "cmd.exe /c " if sys.platform == "win32" else ""
4926
4932
  hook_filter_cmd = f"{_hook_prefix}{shlex.quote(sys.executable)} {shlex.quote(str(cli_dir / 'hook_filter.py'))}"
4927
4933
  hook_read_cmd = f"{_hook_prefix}{shlex.quote(sys.executable)} {shlex.quote(str(cli_dir / 'hook_read.py'))}"
4928
4934
  hook_c3read_cmd = f"{_hook_prefix}{shlex.quote(sys.executable)} {shlex.quote(str(cli_dir / 'hook_c3read.py'))}"
@@ -4962,13 +4968,24 @@ def cmd_install_mcp(args):
4962
4968
  },
4963
4969
  {
4964
4970
  "matcher": read_matcher,
4965
- "hooks": [{"type": "command", "command": hook_read_cmd}]
4971
+ "hooks": [
4972
+ {"type": "command", "command": hook_read_cmd},
4973
+ {"type": "command", "command": hook_ghost_files_cmd},
4974
+ ]
4966
4975
  },
4967
4976
  {
4968
4977
  "matcher": "mcp__c3__c3_read",
4969
4978
  "hooks": [
4970
4979
  {"type": "command", "command": hook_c3read_cmd},
4971
4980
  {"type": "command", "command": hook_c3_signal_cmd},
4981
+ {"type": "command", "command": hook_ghost_files_cmd},
4982
+ ]
4983
+ },
4984
+ {
4985
+ "matcher": "mcp__c3__c3_shell",
4986
+ "hooks": [
4987
+ {"type": "command", "command": hook_c3_signal_cmd},
4988
+ {"type": "command", "command": hook_ghost_files_cmd},
4972
4989
  ]
4973
4990
  },
4974
4991
  {
@@ -212,6 +212,17 @@ def cleanup_ghost_files(ghosts: list[dict]) -> list[str]:
212
212
  return deleted
213
213
 
214
214
 
215
+ # Tools whose output can carry shell-meta text that leaks into 0-byte files:
216
+ # native shells, c3_shell (its `N->Mtok` filter header), and file reads whose
217
+ # content has `-> Type` hints. A downstream shell sees `> word` and creates an
218
+ # empty file named `word`.
219
+ _GHOST_TRIGGER_TOOLS = (
220
+ "Bash", "run_shell_command",
221
+ "mcp__c3__c3_shell",
222
+ "mcp__c3__c3_read", "Read", "read_file",
223
+ )
224
+
225
+
215
226
  def main():
216
227
  try:
217
228
  raw = sys.stdin.read()
@@ -221,8 +232,7 @@ def main():
221
232
  data = json.loads(raw)
222
233
  tool_name = data.get("tool_name", "")
223
234
 
224
- # Only trigger on Bash (Claude Code) or run_shell_command (Gemini)
225
- if tool_name not in ("Bash", "run_shell_command"):
235
+ if tool_name not in _GHOST_TRIGGER_TOOLS:
226
236
  return
227
237
 
228
238
  is_gemini = isinstance(data.get("tool_response", ""), dict)
@@ -35,6 +35,26 @@ from services.tool_classifier import CATEGORIES
35
35
 
36
36
  app = Flask(__name__, static_folder=str(Path(__file__).parent))
37
37
 
38
+ # Localhost-only security: Host-header allowlist + Origin/Referer CSRF guard +
39
+ # scoped CORS. The hub manages MANY projects and exposes command-executing
40
+ # endpoints (launch-ide, mcp-server-add, permissions), so cross-origin CSRF /
41
+ # DNS-rebinding protection matters even though it binds loopback by default.
42
+ # Reads bind host + optional allowed_hosts per-request from hub_config.json.
43
+ from core.web_security import (
44
+ allowed_hostnames as _allowed_hostnames,
45
+ )
46
+ from core.web_security import (
47
+ install_guard as _install_web_guard,
48
+ )
49
+
50
+
51
+ def _hub_allowed_hosts():
52
+ _c = _read_hub_config()
53
+ return _allowed_hostnames(_c.get("host"), _c.get("allowed_hosts"))
54
+
55
+
56
+ _install_web_guard(app, _hub_allowed_hosts)
57
+
38
58
  # ─── Hub config ───────────────────────────────────────────────────────────────
39
59
 
40
60
  _GLOBAL_C3_DIR = Path.home() / ".c3"
@@ -160,46 +180,12 @@ def _project_mcp_config_path(project_root: Path, profile) -> Path:
160
180
  return (Path.home() / profile.config_path) if profile.config_path_global else (project_root / profile.config_path)
161
181
 
162
182
 
163
- def _parse_toml_mcp_servers(content: str) -> dict:
164
- servers = {}
165
- current_server = None
166
-
167
- for raw in content.splitlines():
168
- line = raw.split("#", 1)[0].strip()
169
- if not line:
170
- continue
171
-
172
- if line.startswith("[") and line.endswith("]"):
173
- section = line[1:-1].strip()
174
- if section.startswith("mcp_servers."):
175
- current_server = section.split(".", 1)[1]
176
- servers.setdefault(current_server, {})
177
- else:
178
- current_server = None
179
- continue
180
-
181
- if not current_server or "=" not in line:
182
- continue
183
-
184
- key, value = line.split("=", 1)
185
- key = key.strip().strip('"')
186
- value = value.strip()
187
-
188
- if key == "args":
189
- servers[current_server]["args"] = re.findall(r"[\"']([^\"']*)[\"']", value)
190
- elif key in ("command", "type"):
191
- match = re.match(r"^[\"'](.*)[\"']$", value)
192
- servers[current_server][key] = match.group(1) if match else value
193
- elif key == "enabled":
194
- low = value.lower()
195
- if low.startswith("true"):
196
- servers[current_server]["enabled"] = True
197
- elif low.startswith("false"):
198
- servers[current_server]["enabled"] = False
199
- else:
200
- servers[current_server][key] = value
201
-
202
- return servers
183
+ from core.mcp_toml import (
184
+ parse_toml_mcp_servers as _parse_toml_mcp_servers,
185
+ )
186
+ from core.mcp_toml import (
187
+ upsert_toml_section as _upsert_toml_section,
188
+ )
203
189
 
204
190
 
205
191
  def _read_project_mcp_servers_for_profile(profile, mcp_file: Path) -> tuple[dict, dict]:
@@ -218,73 +204,6 @@ def _read_project_mcp_servers_for_profile(profile, mcp_file: Path) -> tuple[dict
218
204
  return servers, raw_config
219
205
 
220
206
 
221
- def _toml_escape_str(value: str) -> str:
222
- return value.replace("\\", "/")
223
-
224
-
225
- def _upsert_toml_section(toml_path: Path, section: str, entries: dict) -> None:
226
- content = toml_path.read_text(encoding="utf-8") if toml_path.exists() else ""
227
- header = f"[{section}]"
228
-
229
- lines = content.splitlines()
230
- new_lines = []
231
- skip = False
232
- for line in lines:
233
- stripped = line.strip()
234
- if stripped == header:
235
- skip = True
236
- continue
237
- if skip and stripped.startswith("["):
238
- skip = False
239
- if not skip:
240
- new_lines.append(line)
241
-
242
- content = "\n".join(new_lines).rstrip()
243
- section_lines = [f"\n\n{header}"]
244
- for key, value in entries.items():
245
- if isinstance(value, list):
246
- items = ", ".join(f'"{_toml_escape_str(str(item))}"' for item in value)
247
- section_lines.append(f'{key} = [{items}]')
248
- elif isinstance(value, bool):
249
- section_lines.append(f'{key} = {"true" if value else "false"}')
250
- else:
251
- section_lines.append(f'{key} = "{_toml_escape_str(str(value))}"')
252
- section_lines.append("")
253
-
254
- toml_path.parent.mkdir(parents=True, exist_ok=True)
255
- toml_path.write_text(content + "\n".join(section_lines), encoding="utf-8")
256
-
257
-
258
- def _remove_toml_section(toml_path: Path, section: str) -> bool:
259
- if not toml_path.exists():
260
- return False
261
- content = toml_path.read_text(encoding="utf-8")
262
- header = f"[{section}]"
263
-
264
- lines = content.splitlines()
265
- new_lines = []
266
- skip = False
267
- removed = False
268
- for line in lines:
269
- stripped = line.strip()
270
- if stripped == header:
271
- skip = True
272
- removed = True
273
- continue
274
- if skip and stripped.startswith("["):
275
- skip = False
276
- if not skip:
277
- new_lines.append(line)
278
-
279
- if removed:
280
- remaining = "\n".join(new_lines).rstrip()
281
- if remaining:
282
- toml_path.write_text(remaining + "\n", encoding="utf-8")
283
- else:
284
- toml_path.unlink()
285
- return removed
286
-
287
-
288
207
  def _build_mcp_cli_capabilities() -> dict:
289
208
  return {
290
209
  "commands": [
@@ -519,6 +438,11 @@ def api_projects_open():
519
438
  path = Path(path_str).resolve()
520
439
  if not path.exists():
521
440
  return jsonify({"error": f"Path does not exist: {path_str}"}), 404
441
+ # Only ever open directories. Opening a *file* via os.startfile would
442
+ # invoke its default handler (e.g. run an .exe/.bat/.lnk), so refuse
443
+ # anything that is not a folder.
444
+ if not path.is_dir():
445
+ return jsonify({"error": "Only directories can be opened"}), 400
522
446
 
523
447
  if sys.platform == "win32":
524
448
  os.startfile(str(path))
@@ -639,7 +639,9 @@ async def c3_shell(cmd: str, cwd: str = "", timeout: int = 60,
639
639
  """EXECUTE shell command — structured returns, auto-filter, ledger-aware.
640
640
  Use for tests, git, build, scripts. Returns exit_code/stdout/stderr/duration_ms.
641
641
  Auto-filters stdout >30 lines; auto-logs git mutations to the edit ledger.
642
- Blocks: rm -rf / or ~, fork bombs. Soft-warns on --force, --no-verify, reset --hard.
642
+ Best-effort block of catastrophic commands (rm -rf of /, a top-level system dir, or
643
+ $HOME/~; fork bombs; whole-drive wipes) — a guard, NOT a sandbox. Soft-warns on
644
+ --force, --no-verify, reset --hard.
643
645
  Native Bash remains the fallback for interactive/TTY commands."""
644
646
  svc = _svc(ctx)
645
647
 
@@ -10,7 +10,6 @@ import csv
10
10
  import json
11
11
  import logging
12
12
  import os
13
- import re
14
13
  import signal
15
14
  import subprocess
16
15
  import sys
@@ -167,12 +166,21 @@ atexit.register(_cleanup_runtime)
167
166
 
168
167
 
169
168
  # ─── CORS middleware ──────────────────────────────────────
170
- @app.after_request
171
- def add_cors(response):
172
- response.headers['Access-Control-Allow-Origin'] = '*'
173
- response.headers['Access-Control-Allow-Headers'] = 'Content-Type'
174
- response.headers['Access-Control-Allow-Methods'] = 'GET,POST,DELETE,OPTIONS'
175
- return response
169
+ # Localhost-only security: Host-header allowlist + Origin/Referer CSRF guard +
170
+ # scoped CORS (no wildcard). This UI server always binds 127.0.0.1, so only
171
+ # loopback origins are accepted. A loopback bind alone does NOT stop a web page
172
+ # in the user's browser from driving these endpoints — see core/web_security.py.
173
+ from core.web_security import (
174
+ allowed_hostnames as _allowed_hostnames,
175
+ )
176
+ from core.web_security import (
177
+ guard_summary as _guard_summary,
178
+ )
179
+ from core.web_security import (
180
+ install_guard as _install_web_guard,
181
+ )
182
+
183
+ _install_web_guard(app, lambda: _allowed_hostnames(None))
176
184
 
177
185
 
178
186
  # ─── Serve the UI ─────────────────────────────────────────
@@ -283,6 +291,11 @@ def api_projects_open():
283
291
  path = Path(path_str).resolve()
284
292
  if not path.exists():
285
293
  return jsonify({"error": f"Path does not exist: {path_str}"}), 404
294
+ # Only ever open directories. Opening a *file* via os.startfile would
295
+ # invoke its default handler (e.g. run an .exe/.bat/.lnk), so refuse
296
+ # anything that is not a folder.
297
+ if not path.is_dir():
298
+ return jsonify({"error": "Only directories can be opened"}), 400
286
299
 
287
300
  if sys.platform == "win32":
288
301
  os.startfile(str(path))
@@ -365,7 +378,8 @@ def api_health():
365
378
  except Exception:
366
379
  pass
367
380
 
368
- return jsonify({"service": "c3-ui", "sources": sources, "session": session_info})
381
+ return jsonify({"service": "c3-ui", "sources": sources, "session": session_info,
382
+ "web_guard": _guard_summary()})
369
383
 
370
384
 
371
385
  # ─── API: Session Registry ───────────────────────────────
@@ -2410,47 +2424,15 @@ def api_proxy_tools():
2410
2424
 
2411
2425
 
2412
2426
  # ─── API: MCP Status ─────────────────────────────────────
2413
- def _parse_toml_mcp_servers(content: str) -> dict:
2414
- """Parse [mcp_servers.<name>] sections from TOML content."""
2415
- servers = {}
2416
- current_server = None
2417
-
2418
- for raw in content.splitlines():
2419
- line = raw.split("#", 1)[0].strip()
2420
- if not line:
2421
- continue
2422
-
2423
- if line.startswith("[") and line.endswith("]"):
2424
- section = line[1:-1].strip()
2425
- if section.startswith("mcp_servers."):
2426
- current_server = section.split(".", 1)[1]
2427
- servers.setdefault(current_server, {})
2428
- else:
2429
- current_server = None
2430
- continue
2431
-
2432
- if not current_server or "=" not in line:
2433
- continue
2434
-
2435
- key, value = line.split("=", 1)
2436
- key = key.strip()
2437
- value = value.strip()
2438
-
2439
- if key == "args":
2440
- servers[current_server]["args"] = re.findall(r"[\"']([^\"']*)[\"']", value)
2441
- elif key in ("command", "type"):
2442
- match = re.match(r"^[\"'](.*)[\"']$", value)
2443
- servers[current_server][key] = match.group(1) if match else value
2444
- elif key == "enabled":
2445
- low = value.lower()
2446
- if low.startswith("true"):
2447
- servers[current_server]["enabled"] = True
2448
- elif low.startswith("false"):
2449
- servers[current_server]["enabled"] = False
2450
- else:
2451
- servers[current_server][key] = value
2452
-
2453
- return servers
2427
+ from core.mcp_toml import (
2428
+ parse_toml_mcp_servers as _parse_toml_mcp_servers,
2429
+ )
2430
+ from core.mcp_toml import (
2431
+ remove_toml_section as _remove_toml_section,
2432
+ )
2433
+ from core.mcp_toml import (
2434
+ upsert_toml_section as _upsert_toml_section,
2435
+ )
2454
2436
 
2455
2437
 
2456
2438
  def _find_server_script(servers: dict) -> bool:
@@ -2463,71 +2445,6 @@ def _find_server_script(servers: dict) -> bool:
2463
2445
  return False
2464
2446
 
2465
2447
 
2466
- def _toml_escape_str(value: str) -> str:
2467
- return value.replace("\\", "/")
2468
-
2469
-
2470
- def _upsert_toml_section(toml_path: Path, section: str, entries: dict) -> None:
2471
- """Add or replace a dotted TOML section in-place."""
2472
- content = toml_path.read_text(encoding="utf-8") if toml_path.exists() else ""
2473
- header = f"[{section}]"
2474
-
2475
- lines = content.splitlines()
2476
- new_lines = []
2477
- skip = False
2478
- for line in lines:
2479
- stripped = line.strip()
2480
- if stripped == header:
2481
- skip = True
2482
- continue
2483
- if skip and stripped.startswith("["):
2484
- skip = False
2485
- if not skip:
2486
- new_lines.append(line)
2487
-
2488
- content = "\n".join(new_lines).rstrip()
2489
- section_lines = [f"\n\n{header}"]
2490
- for k, v in entries.items():
2491
- if isinstance(v, list):
2492
- items = ", ".join(f'"{_toml_escape_str(str(x))}"' for x in v)
2493
- section_lines.append(f'{k} = [{items}]')
2494
- elif isinstance(v, bool):
2495
- section_lines.append(f'{k} = {"true" if v else "false"}')
2496
- else:
2497
- section_lines.append(f'{k} = "{_toml_escape_str(str(v))}"')
2498
- section_lines.append("")
2499
-
2500
- toml_path.parent.mkdir(parents=True, exist_ok=True)
2501
- toml_path.write_text(content + "\n".join(section_lines), encoding="utf-8")
2502
-
2503
-
2504
- def _remove_toml_section(toml_path: Path, section: str) -> bool:
2505
- """Remove a dotted TOML section. Returns True if removed."""
2506
- if not toml_path.exists():
2507
- return False
2508
- content = toml_path.read_text(encoding="utf-8")
2509
- header = f"[{section}]"
2510
-
2511
- lines = content.splitlines()
2512
- new_lines = []
2513
- skip = False
2514
- removed = False
2515
- for line in lines:
2516
- stripped = line.strip()
2517
- if stripped == header:
2518
- skip = True
2519
- removed = True
2520
- continue
2521
- if skip and stripped.startswith("["):
2522
- skip = False
2523
- if not skip:
2524
- new_lines.append(line)
2525
-
2526
- if removed:
2527
- toml_path.write_text("\n".join(new_lines).rstrip() + "\n", encoding="utf-8")
2528
- return removed
2529
-
2530
-
2531
2448
  def _resolve_mcp_profile(ide_name: str | None):
2532
2449
  requested = (ide_name or "").strip().lower()
2533
2450
  if requested and requested != "auto":
@@ -64,10 +64,10 @@ def _filter_text(text: str, depth: str, svc, finalize) -> str:
64
64
  raw_tokens = res['raw_tokens']
65
65
  savings_pct = round((1 - filtered_tokens / raw_tokens) * 100, 1) if raw_tokens > 0 else 0
66
66
 
67
- header = f"[filter:{method}] {raw_tokens}->{filtered_tokens}tok ({savings_pct}%saved)"
67
+ header = f"[filter:{method}] {raw_tokens}{filtered_tokens}tok ({savings_pct}%saved)"
68
68
  resp = f"{header}\n{result_text}"
69
69
  return finalize("c3_filter", {"depth": depth},
70
- resp, f"{raw_tokens}->{filtered_tokens}tok",
70
+ resp, f"{raw_tokens}{filtered_tokens}tok",
71
71
  response_tokens=filtered_tokens)
72
72
 
73
73
 
@@ -25,13 +25,50 @@ def _coerce_list(val: Any) -> list[str] | None:
25
25
  except (json.JSONDecodeError, ValueError):
26
26
  pass
27
27
  if val:
28
+ # Comma-separated symbols ("a,b,c") -> multiple targets. Function/class
29
+ # names never contain commas, and regex anchors (^foo$) have none either.
30
+ if "," in val:
31
+ return [s.strip() for s in val.split(",") if s.strip()]
28
32
  return [val]
29
33
  return None
30
34
 
31
35
 
36
+ def _coerce_lines(val: Any):
37
+ """Coerce `lines` from MCP's string serialization into an int or list.
38
+
39
+ MCP clients sometimes serialize numbers/lists as strings (the same reason
40
+ `_coerce_list` exists for `symbols`). Without this, a JSON-string such as
41
+ "[22, 193]" or "22" falls through handle_read's range logic and the tool
42
+ silently returns the file *map* instead of the requested source lines.
43
+ """
44
+ if val is None or isinstance(val, (int, list, tuple)):
45
+ return val
46
+ if isinstance(val, str):
47
+ val = val.strip()
48
+ if not val:
49
+ return None
50
+ if val.startswith("["):
51
+ try:
52
+ parsed = json.loads(val)
53
+ except (json.JSONDecodeError, ValueError):
54
+ return None
55
+ return parsed if isinstance(parsed, list) else None
56
+ try:
57
+ return int(val)
58
+ except ValueError:
59
+ if "-" in val: # "start-end" like "22-40"
60
+ a, _, b = val.partition("-")
61
+ try:
62
+ return [int(a.strip()), int(b.strip())]
63
+ except ValueError:
64
+ return None
65
+ return None
66
+
67
+
32
68
  def handle_read(file_path: str, symbols: Any = None, lines: Any = None,
33
69
  include_docstrings: bool = True, svc=None, finalize=None) -> str:
34
70
  symbols = _coerce_list(symbols)
71
+ lines = _coerce_lines(lines)
35
72
  # Multi-file dispatch (parallel)
36
73
  if "," in file_path:
37
74
  paths = [p.strip() for p in file_path.split(",") if p.strip()]