code-context-control 2.32.1__tar.gz → 2.33.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 (199) hide show
  1. {code_context_control-2.32.1 → code_context_control-2.33.0}/PKG-INFO +31 -2
  2. {code_context_control-2.32.1 → code_context_control-2.33.0}/README.md +30 -1
  3. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/c3.py +21 -4
  4. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/hook_ghost_files.py +12 -2
  5. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/hub_server.py +25 -0
  6. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/mcp_server.py +3 -1
  7. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/server.py +17 -6
  8. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/tools/filter.py +2 -2
  9. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/tools/read.py +37 -0
  10. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/tools/shell.py +22 -2
  11. {code_context_control-2.32.1 → code_context_control-2.33.0}/code_context_control.egg-info/PKG-INFO +31 -2
  12. {code_context_control-2.32.1 → code_context_control-2.33.0}/code_context_control.egg-info/SOURCES.txt +3 -0
  13. code_context_control-2.33.0/core/web_security.py +158 -0
  14. {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/mcp_oracle.py +13 -6
  15. {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/oracle_server.py +15 -6
  16. {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/services/tool_registry.py +17 -2
  17. {code_context_control-2.32.1 → code_context_control-2.33.0}/pyproject.toml +1 -1
  18. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/claude_md.py +1 -1
  19. {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_c3_shell.py +34 -0
  20. code_context_control-2.33.0/tests/test_read_coercion.py +68 -0
  21. code_context_control-2.33.0/tests/test_web_security.py +106 -0
  22. {code_context_control-2.32.1 → code_context_control-2.33.0}/LICENSE +0 -0
  23. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/__init__.py +0 -0
  24. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/_hook_utils.py +0 -0
  25. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/commands/__init__.py +0 -0
  26. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/commands/common.py +0 -0
  27. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/commands/parser.py +0 -0
  28. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/docs.html +0 -0
  29. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/edits.html +0 -0
  30. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/hook_auto_snapshot.py +0 -0
  31. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/hook_c3_signal.py +0 -0
  32. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/hook_c3read.py +0 -0
  33. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/hook_edit_ledger.py +0 -0
  34. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/hook_edit_unlock.py +0 -0
  35. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/hook_filter.py +0 -0
  36. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/hook_pretool_enforce.py +0 -0
  37. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/hook_read.py +0 -0
  38. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/hook_session_stats.py +0 -0
  39. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/hook_terse_advisor.py +0 -0
  40. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/hub.html +0 -0
  41. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/mcp_proxy.py +0 -0
  42. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/tools/__init__.py +0 -0
  43. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/tools/_helpers.py +0 -0
  44. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/tools/agent.py +0 -0
  45. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/tools/bitbucket.py +0 -0
  46. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/tools/compress.py +0 -0
  47. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/tools/delegate.py +0 -0
  48. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/tools/edit.py +0 -0
  49. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/tools/edits.py +0 -0
  50. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/tools/impact.py +0 -0
  51. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/tools/memory.py +0 -0
  52. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/tools/project.py +0 -0
  53. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/tools/search.py +0 -0
  54. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/tools/session.py +0 -0
  55. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/tools/status.py +0 -0
  56. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/tools/validate.py +0 -0
  57. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/ui/api.js +0 -0
  58. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/ui/app.js +0 -0
  59. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/ui/components/bitbucket.js +0 -0
  60. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/ui/components/chat.js +0 -0
  61. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/ui/components/dashboard.js +0 -0
  62. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/ui/components/edits.js +0 -0
  63. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/ui/components/instructions.js +0 -0
  64. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/ui/components/memory.js +0 -0
  65. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/ui/components/sessions.js +0 -0
  66. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/ui/components/settings.js +0 -0
  67. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/ui/components/sidebar.js +0 -0
  68. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/ui/icons.js +0 -0
  69. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/ui/shared.js +0 -0
  70. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/ui/theme.js +0 -0
  71. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/ui.html +0 -0
  72. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/ui_legacy.html +0 -0
  73. {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/ui_nano.html +0 -0
  74. {code_context_control-2.32.1 → code_context_control-2.33.0}/code_context_control.egg-info/dependency_links.txt +0 -0
  75. {code_context_control-2.32.1 → code_context_control-2.33.0}/code_context_control.egg-info/entry_points.txt +0 -0
  76. {code_context_control-2.32.1 → code_context_control-2.33.0}/code_context_control.egg-info/requires.txt +0 -0
  77. {code_context_control-2.32.1 → code_context_control-2.33.0}/code_context_control.egg-info/top_level.txt +0 -0
  78. {code_context_control-2.32.1 → code_context_control-2.33.0}/core/__init__.py +0 -0
  79. {code_context_control-2.32.1 → code_context_control-2.33.0}/core/config.py +0 -0
  80. {code_context_control-2.32.1 → code_context_control-2.33.0}/core/ide.py +0 -0
  81. {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/__init__.py +0 -0
  82. {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/config.py +0 -0
  83. {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/oracle.html +0 -0
  84. {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/services/__init__.py +0 -0
  85. {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/services/api_auth.py +0 -0
  86. {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/services/c3_bridge.py +0 -0
  87. {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/services/chat_engine.py +0 -0
  88. {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/services/chat_store.py +0 -0
  89. {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/services/cross_memory.py +0 -0
  90. {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/services/federated_graph.py +0 -0
  91. {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/services/health_checker.py +0 -0
  92. {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/services/insight_engine.py +0 -0
  93. {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/services/memory_reader.py +0 -0
  94. {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/services/memory_writer.py +0 -0
  95. {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/services/ollama_bridge.py +0 -0
  96. {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/services/project_scanner.py +0 -0
  97. {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/services/review_agent.py +0 -0
  98. {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/services/tool_executor.py +0 -0
  99. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/__init__.py +0 -0
  100. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/activity_log.py +0 -0
  101. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/agent_base.py +0 -0
  102. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/agents.py +0 -0
  103. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/auto_memory.py +0 -0
  104. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/bench/__init__.py +0 -0
  105. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/bench/external/__init__.py +0 -0
  106. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/bench/external/aider_polyglot.py +0 -0
  107. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/bench/external/swe_bench.py +0 -0
  108. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/benchmark_dashboard.py +0 -0
  109. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/bitbucket_client.py +0 -0
  110. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/bitbucket_credentials.py +0 -0
  111. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/compressor.py +0 -0
  112. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/context_snapshot.py +0 -0
  113. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/conversation_store.py +0 -0
  114. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/doc_index.py +0 -0
  115. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/e2e_benchmark.py +0 -0
  116. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/e2e_evaluator.py +0 -0
  117. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/e2e_tasks.py +0 -0
  118. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/edit_ledger.py +0 -0
  119. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/embedding_index.py +0 -0
  120. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/error_reporting.py +0 -0
  121. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/file_memory.py +0 -0
  122. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/hub_service.py +0 -0
  123. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/indexer.py +0 -0
  124. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/memory.py +0 -0
  125. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/memory_consolidator.py +0 -0
  126. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/memory_graph.py +0 -0
  127. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/memory_grounder.py +0 -0
  128. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/memory_scorer.py +0 -0
  129. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/metrics.py +0 -0
  130. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/notifications.py +0 -0
  131. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/ollama_client.py +0 -0
  132. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/output_filter.py +0 -0
  133. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/parser.py +0 -0
  134. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/project_manager.py +0 -0
  135. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/project_runtime.py +0 -0
  136. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/protocol.py +0 -0
  137. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/proxy_state.py +0 -0
  138. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/retrieval_broker.py +0 -0
  139. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/router.py +0 -0
  140. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/runtime.py +0 -0
  141. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/session_benchmark.py +0 -0
  142. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/session_manager.py +0 -0
  143. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/session_preloader.py +0 -0
  144. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/text_index.py +0 -0
  145. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/tool_classifier.py +0 -0
  146. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/transcript_index.py +0 -0
  147. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/validation_cache.py +0 -0
  148. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/vector_store.py +0 -0
  149. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/version_tracker.py +0 -0
  150. {code_context_control-2.32.1 → code_context_control-2.33.0}/services/watcher.py +0 -0
  151. {code_context_control-2.32.1 → code_context_control-2.33.0}/setup.cfg +0 -0
  152. {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_aider_polyglot.py +0 -0
  153. {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_bitbucket_cli_smoke.py +0 -0
  154. {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_bitbucket_client.py +0 -0
  155. {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_bitbucket_credentials.py +0 -0
  156. {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_bitbucket_tool.py +0 -0
  157. {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_cli_smoke.py +0 -0
  158. {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_e2e_benchmark.py +0 -0
  159. {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_edit_normalization.py +0 -0
  160. {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_enforcement_flip.py +0 -0
  161. {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_federated_graph.py +0 -0
  162. {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_ghost_files.py +0 -0
  163. {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_hub_server_smoke.py +0 -0
  164. {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_mcp_server_smoke.py +0 -0
  165. {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_memory_graph_api.py +0 -0
  166. {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_memory_system.py +0 -0
  167. {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_notification_discipline.py +0 -0
  168. {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_oracle_api_auth.py +0 -0
  169. {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_oracle_apikey_api.py +0 -0
  170. {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_oracle_discovery_api.py +0 -0
  171. {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_output_filter.py +0 -0
  172. {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_permissions.py +0 -0
  173. {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_project_manager.py +0 -0
  174. {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_project_manager_merge.py +0 -0
  175. {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_project_tool.py +0 -0
  176. {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_session_benchmark.py +0 -0
  177. {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_session_budget.py +0 -0
  178. {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_swe_bench.py +0 -0
  179. {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_tool_registry.py +0 -0
  180. {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_validate.py +0 -0
  181. {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_windows_reliability.py +0 -0
  182. {code_context_control-2.32.1 → code_context_control-2.33.0}/tui/__init__.py +0 -0
  183. {code_context_control-2.32.1 → code_context_control-2.33.0}/tui/backend.py +0 -0
  184. {code_context_control-2.32.1 → code_context_control-2.33.0}/tui/main.py +0 -0
  185. {code_context_control-2.32.1 → code_context_control-2.33.0}/tui/screens/__init__.py +0 -0
  186. {code_context_control-2.32.1 → code_context_control-2.33.0}/tui/screens/benchmark_view.py +0 -0
  187. {code_context_control-2.32.1 → code_context_control-2.33.0}/tui/screens/claudemd_view.py +0 -0
  188. {code_context_control-2.32.1 → code_context_control-2.33.0}/tui/screens/compress_view.py +0 -0
  189. {code_context_control-2.32.1 → code_context_control-2.33.0}/tui/screens/index_view.py +0 -0
  190. {code_context_control-2.32.1 → code_context_control-2.33.0}/tui/screens/init_view.py +0 -0
  191. {code_context_control-2.32.1 → code_context_control-2.33.0}/tui/screens/mcp_view.py +0 -0
  192. {code_context_control-2.32.1 → code_context_control-2.33.0}/tui/screens/optimize_view.py +0 -0
  193. {code_context_control-2.32.1 → code_context_control-2.33.0}/tui/screens/pipe_view.py +0 -0
  194. {code_context_control-2.32.1 → code_context_control-2.33.0}/tui/screens/projects_view.py +0 -0
  195. {code_context_control-2.32.1 → code_context_control-2.33.0}/tui/screens/search_view.py +0 -0
  196. {code_context_control-2.32.1 → code_context_control-2.33.0}/tui/screens/session_view.py +0 -0
  197. {code_context_control-2.32.1 → code_context_control-2.33.0}/tui/screens/stats.py +0 -0
  198. {code_context_control-2.32.1 → code_context_control-2.33.0}/tui/screens/ui_view.py +0 -0
  199. {code_context_control-2.32.1 → code_context_control-2.33.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.1
3
+ Version: 2.33.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
@@ -281,6 +281,34 @@ to the C3 edit ledger so the audit trail covers platform-side changes too.
281
281
  The **Hub UI** (per-project) gains a "Bitbucket" tab with sub-views for
282
282
  Overview / Pull Requests / Branches / Activity / Admin.
283
283
 
284
+ ### Oracle Discovery API (v2.32.0)
285
+
286
+ The **Oracle** is C3's optional cross-project memory agent (a local web app). As of
287
+ v2.32.0 it can expose C3's cross-project code & memory intelligence as **tools for an
288
+ external LLM** — point Claude (or any function-calling model) at a running Oracle and
289
+ it can discover your projects and search code, memory, and the cross-project graph
290
+ across all of them.
291
+
292
+ Two transports share one tool core:
293
+
294
+ - **MCP** (streamable HTTP/SSE) at `http://127.0.0.1:3332/mcp` — native for Claude
295
+ Code / Claude Desktop / any MCP client.
296
+ - **OpenAPI REST** at `http://127.0.0.1:3331/api/discovery` — for any LLM with
297
+ function-calling (fetch `/openapi.json` to auto-register the tools).
298
+
299
+ ```bash
300
+ # Start the Oracle (serves the REST + MCP discovery endpoints)
301
+ python oracle/oracle_server.py --no-browser
302
+
303
+ # Print the Bearer token + a ready-to-paste .mcp.json snippet
304
+ c3 oracle api info
305
+ ```
306
+
307
+ Only **read** and **safe-action** tools are exposed (no code editing); requests need a
308
+ **Bearer token** (stored in the OS keyring) and both servers bind `127.0.0.1` by
309
+ default. Generate, rotate, and copy the token from the dashboard's **Settings →
310
+ Discovery API** tab. See the [Oracle Discovery API guide](oracle-guide/discovery-api.md).
311
+
284
312
  ---
285
313
 
286
314
  ## Tiered local AI (optional)
@@ -335,7 +363,7 @@ Real-world A/B tests: same task, with and without C3 mounted. Reports include to
335
363
 
336
364
  ## Security & privacy
337
365
 
338
- - **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.
339
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.
340
368
  - **API keys** for third-party model providers are read from environment variables and never persisted by C3.
341
369
  - See [`SECURITY.md`](SECURITY.md) for the full hardening guide and disclosure policy.
@@ -355,6 +383,7 @@ The author may introduce a paid offering or relicense future major versions; no
355
383
 
356
384
  - **PyPI:** https://pypi.org/project/code-context-control/
357
385
  - **Changelog:** [`CHANGELOG.md`](CHANGELOG.md)
386
+ - **Oracle Discovery API:** [`oracle-guide/discovery-api.md`](oracle-guide/discovery-api.md)
358
387
  - **Security policy:** [`SECURITY.md`](SECURITY.md)
359
388
  - **Licensing FAQ:** [`LICENSING.md`](LICENSING.md)
360
389
  - **Issues:** https://github.com/drknowhow/code-context-control/issues
@@ -219,6 +219,34 @@ to the C3 edit ledger so the audit trail covers platform-side changes too.
219
219
  The **Hub UI** (per-project) gains a "Bitbucket" tab with sub-views for
220
220
  Overview / Pull Requests / Branches / Activity / Admin.
221
221
 
222
+ ### Oracle Discovery API (v2.32.0)
223
+
224
+ The **Oracle** is C3's optional cross-project memory agent (a local web app). As of
225
+ v2.32.0 it can expose C3's cross-project code & memory intelligence as **tools for an
226
+ external LLM** — point Claude (or any function-calling model) at a running Oracle and
227
+ it can discover your projects and search code, memory, and the cross-project graph
228
+ across all of them.
229
+
230
+ Two transports share one tool core:
231
+
232
+ - **MCP** (streamable HTTP/SSE) at `http://127.0.0.1:3332/mcp` — native for Claude
233
+ Code / Claude Desktop / any MCP client.
234
+ - **OpenAPI REST** at `http://127.0.0.1:3331/api/discovery` — for any LLM with
235
+ function-calling (fetch `/openapi.json` to auto-register the tools).
236
+
237
+ ```bash
238
+ # Start the Oracle (serves the REST + MCP discovery endpoints)
239
+ python oracle/oracle_server.py --no-browser
240
+
241
+ # Print the Bearer token + a ready-to-paste .mcp.json snippet
242
+ c3 oracle api info
243
+ ```
244
+
245
+ Only **read** and **safe-action** tools are exposed (no code editing); requests need a
246
+ **Bearer token** (stored in the OS keyring) and both servers bind `127.0.0.1` by
247
+ default. Generate, rotate, and copy the token from the dashboard's **Settings →
248
+ Discovery API** tab. See the [Oracle Discovery API guide](oracle-guide/discovery-api.md).
249
+
222
250
  ---
223
251
 
224
252
  ## Tiered local AI (optional)
@@ -273,7 +301,7 @@ Real-world A/B tests: same task, with and without C3 mounted. Reports include to
273
301
 
274
302
  ## Security & privacy
275
303
 
276
- - **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.
277
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.
278
306
  - **API keys** for third-party model providers are read from environment variables and never persisted by C3.
279
307
  - See [`SECURITY.md`](SECURITY.md) for the full hardening guide and disclosure policy.
@@ -293,6 +321,7 @@ The author may introduce a paid offering or relicense future major versions; no
293
321
 
294
322
  - **PyPI:** https://pypi.org/project/code-context-control/
295
323
  - **Changelog:** [`CHANGELOG.md`](CHANGELOG.md)
324
+ - **Oracle Discovery API:** [`oracle-guide/discovery-api.md`](oracle-guide/discovery-api.md)
296
325
  - **Security policy:** [`SECURITY.md`](SECURITY.md)
297
326
  - **Licensing FAQ:** [`LICENSING.md`](LICENSING.md)
298
327
  - **Issues:** https://github.com/drknowhow/code-context-control/issues
@@ -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.1"
88
+ __version__ = "2.33.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"
@@ -519,6 +539,11 @@ def api_projects_open():
519
539
  path = Path(path_str).resolve()
520
540
  if not path.exists():
521
541
  return jsonify({"error": f"Path does not exist: {path_str}"}), 404
542
+ # Only ever open directories. Opening a *file* via os.startfile would
543
+ # invoke its default handler (e.g. run an .exe/.bat/.lnk), so refuse
544
+ # anything that is not a folder.
545
+ if not path.is_dir():
546
+ return jsonify({"error": "Only directories can be opened"}), 400
522
547
 
523
548
  if sys.platform == "win32":
524
549
  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
 
@@ -167,12 +167,18 @@ atexit.register(_cleanup_runtime)
167
167
 
168
168
 
169
169
  # ─── 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
170
+ # Localhost-only security: Host-header allowlist + Origin/Referer CSRF guard +
171
+ # scoped CORS (no wildcard). This UI server always binds 127.0.0.1, so only
172
+ # loopback origins are accepted. A loopback bind alone does NOT stop a web page
173
+ # in the user's browser from driving these endpoints — see core/web_security.py.
174
+ from core.web_security import (
175
+ allowed_hostnames as _allowed_hostnames,
176
+ )
177
+ from core.web_security import (
178
+ install_guard as _install_web_guard,
179
+ )
180
+
181
+ _install_web_guard(app, lambda: _allowed_hostnames(None))
176
182
 
177
183
 
178
184
  # ─── Serve the UI ─────────────────────────────────────────
@@ -283,6 +289,11 @@ def api_projects_open():
283
289
  path = Path(path_str).resolve()
284
290
  if not path.exists():
285
291
  return jsonify({"error": f"Path does not exist: {path_str}"}), 404
292
+ # Only ever open directories. Opening a *file* via os.startfile would
293
+ # invoke its default handler (e.g. run an .exe/.bat/.lnk), so refuse
294
+ # anything that is not a folder.
295
+ if not path.is_dir():
296
+ return jsonify({"error": "Only directories can be opened"}), 400
286
297
 
287
298
  if sys.platform == "win32":
288
299
  os.startfile(str(path))
@@ -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()]
@@ -22,9 +22,29 @@ from core import count_tokens
22
22
  _GIT_MUTATING = re.compile(
23
23
  r"^\s*git\s+(commit|add|mv|rm|merge|rebase|cherry-pick|revert|reset|restore|checkout)\b"
24
24
  )
25
- # Hard deny — fork bombs, rm -rf on root/home. Escape hatch: native Bash.
25
+ # Hard deny — the handful of genuinely catastrophic, irreversible commands.
26
+ # This is a BEST-EFFORT guard, NOT a sandbox: c3_shell runs arbitrary commands
27
+ # by design and a determined caller can trivially reword around these patterns.
28
+ # The escape hatch for an intentional dangerous command is native Bash.
29
+ # Covered: rm -rf of the filesystem root / a top-level system dir / $HOME / ~,
30
+ # the classic fork bomb, and Windows whole-drive-root wipes (del/rd/format C:\).
31
+ # A top-level system dir only matches when it is the *whole* target, so deleting
32
+ # a nested path like /home/me/project/build is intentionally NOT blocked.
26
33
  _BLOCKED = re.compile(
27
- r"(\brm\s+-rf\s+(/|~|\$HOME)(\s|$)|:\(\)\s*\{\s*:\s*\|\s*:)"
34
+ r"""
35
+ (?<!git\ )\brm\b (?:\s+-\S+)* \s+ # rm + any flags, then a target:
36
+ (?:
37
+ /(?=\s|$|\*) # filesystem root: / /*
38
+ | ~(?=/|\s|$) # home dir: ~ ~/
39
+ | \$HOME\b # $HOME
40
+ | /(?:etc|usr|bin|sbin|lib|lib64|var|boot|root|home|srv|sys|proc|dev|opt)
41
+ (?=/?(?:\s|$|\*)) # a whole top-level system dir
42
+ )
43
+ | :\(\)\s*\{\s*:\s*\|\s*:\s*\}? # fork bomb :(){ :|: };:
44
+ | \b(?:format|rd|rmdir|del)\b [^\n]*? # windows whole-drive-root wipe
45
+ \b[a-zA-Z]:\\?(?=\s|\*|$|["'])
46
+ """,
47
+ re.IGNORECASE | re.VERBOSE,
28
48
  )
29
49
  # Soft warn — run but prepend a caveat to the response.
30
50
  # `(?<!\w)` / `(?!\w)` anchor against word chars, so `--force` (which starts
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-context-control
3
- Version: 2.32.1
3
+ Version: 2.33.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
@@ -281,6 +281,34 @@ to the C3 edit ledger so the audit trail covers platform-side changes too.
281
281
  The **Hub UI** (per-project) gains a "Bitbucket" tab with sub-views for
282
282
  Overview / Pull Requests / Branches / Activity / Admin.
283
283
 
284
+ ### Oracle Discovery API (v2.32.0)
285
+
286
+ The **Oracle** is C3's optional cross-project memory agent (a local web app). As of
287
+ v2.32.0 it can expose C3's cross-project code & memory intelligence as **tools for an
288
+ external LLM** — point Claude (or any function-calling model) at a running Oracle and
289
+ it can discover your projects and search code, memory, and the cross-project graph
290
+ across all of them.
291
+
292
+ Two transports share one tool core:
293
+
294
+ - **MCP** (streamable HTTP/SSE) at `http://127.0.0.1:3332/mcp` — native for Claude
295
+ Code / Claude Desktop / any MCP client.
296
+ - **OpenAPI REST** at `http://127.0.0.1:3331/api/discovery` — for any LLM with
297
+ function-calling (fetch `/openapi.json` to auto-register the tools).
298
+
299
+ ```bash
300
+ # Start the Oracle (serves the REST + MCP discovery endpoints)
301
+ python oracle/oracle_server.py --no-browser
302
+
303
+ # Print the Bearer token + a ready-to-paste .mcp.json snippet
304
+ c3 oracle api info
305
+ ```
306
+
307
+ Only **read** and **safe-action** tools are exposed (no code editing); requests need a
308
+ **Bearer token** (stored in the OS keyring) and both servers bind `127.0.0.1` by
309
+ default. Generate, rotate, and copy the token from the dashboard's **Settings →
310
+ Discovery API** tab. See the [Oracle Discovery API guide](oracle-guide/discovery-api.md).
311
+
284
312
  ---
285
313
 
286
314
  ## Tiered local AI (optional)
@@ -335,7 +363,7 @@ Real-world A/B tests: same task, with and without C3 mounted. Reports include to
335
363
 
336
364
  ## Security & privacy
337
365
 
338
- - **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.
339
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.
340
368
  - **API keys** for third-party model providers are read from environment variables and never persisted by C3.
341
369
  - See [`SECURITY.md`](SECURITY.md) for the full hardening guide and disclosure policy.
@@ -355,6 +383,7 @@ The author may introduce a paid offering or relicense future major versions; no
355
383
 
356
384
  - **PyPI:** https://pypi.org/project/code-context-control/
357
385
  - **Changelog:** [`CHANGELOG.md`](CHANGELOG.md)
386
+ - **Oracle Discovery API:** [`oracle-guide/discovery-api.md`](oracle-guide/discovery-api.md)
358
387
  - **Security policy:** [`SECURITY.md`](SECURITY.md)
359
388
  - **Licensing FAQ:** [`LICENSING.md`](LICENSING.md)
360
389
  - **Issues:** https://github.com/drknowhow/code-context-control/issues
@@ -69,6 +69,7 @@ code_context_control.egg-info/top_level.txt
69
69
  core/__init__.py
70
70
  core/config.py
71
71
  core/ide.py
72
+ core/web_security.py
72
73
  oracle/__init__.py
73
74
  oracle/config.py
74
75
  oracle/mcp_oracle.py
@@ -168,11 +169,13 @@ tests/test_permissions.py
168
169
  tests/test_project_manager.py
169
170
  tests/test_project_manager_merge.py
170
171
  tests/test_project_tool.py
172
+ tests/test_read_coercion.py
171
173
  tests/test_session_benchmark.py
172
174
  tests/test_session_budget.py
173
175
  tests/test_swe_bench.py
174
176
  tests/test_tool_registry.py
175
177
  tests/test_validate.py
178
+ tests/test_web_security.py
176
179
  tests/test_windows_reliability.py
177
180
  tui/__init__.py
178
181
  tui/backend.py
@@ -0,0 +1,158 @@
1
+ """Localhost-only security guard for C3's Flask dashboards (UI, Hub, Oracle).
2
+
3
+ Why this exists
4
+ ---------------
5
+ C3's web servers bind to loopback (``127.0.0.1``) by default. A loopback bind
6
+ keeps the server off the LAN, but it does **not** protect against requests made
7
+ by a web page running in the user's own browser. Two classic attacks defeat a
8
+ "loopback is safe" assumption:
9
+
10
+ * **Cross-origin / CSRF** — any website the user visits can ``fetch()`` against
11
+ ``http://localhost:<port>/...``. With no auth and a permissive CORS policy,
12
+ that page could drive state-changing endpoints (launch an IDE command, add a
13
+ malicious MCP server, downgrade permissions, wipe data).
14
+ * **DNS rebinding** — an attacker domain re-resolves to ``127.0.0.1`` after the
15
+ page loads, so the browser sends requests to the local server with the
16
+ *attacker's* hostname in the ``Host`` header.
17
+
18
+ This module adds two cheap, standard defenses that together close that gap
19
+ without requiring the dashboard JavaScript to change (same-origin requests pass
20
+ naturally):
21
+
22
+ 1. **Host-header allowlist** — defeats DNS rebinding. The rebound request
23
+ carries the attacker's hostname in ``Host``; anything not in the allowlist is
24
+ rejected.
25
+ 2. **Origin/Referer check on state-changing requests** — defeats cross-origin
26
+ CSRF. Browsers always attach ``Origin`` to ``POST``/``PUT``/``DELETE``/
27
+ ``PATCH`` (cross-origin *and* same-origin), so a mismatched origin is a
28
+ reliable CSRF signal.
29
+
30
+ Non-browser clients (curl, the Oracle Discovery REST/MCP consumers) send no
31
+ ``Origin`` and a loopback ``Host``, so they are unaffected. Bearer-token auth
32
+ (Oracle discovery) still applies on top of this guard.
33
+
34
+ For an intentional non-loopback bind (an explicit, already-warned opt-in), pass
35
+ the configured bind host so the user can still reach their own dashboard; remote
36
+ hosts that need access can be added via the optional ``extra`` argument
37
+ (wired from an ``allowed_hosts`` config list by the caller).
38
+ """
39
+ from __future__ import annotations
40
+
41
+ from collections.abc import Callable, Iterable
42
+ from urllib.parse import urlsplit
43
+
44
+ # Hostnames that always denote "this machine". Note: a literal ``0.0.0.0`` never
45
+ # appears as a browser Host header, so it is intentionally excluded — binding to
46
+ # 0.0.0.0 means the client connects by some concrete IP/name, which must be added
47
+ # via ``extra`` (``allowed_hosts`` config) by the operator.
48
+ _LOOPBACK_HOSTS = frozenset({"localhost", "127.0.0.1", "::1", "[::1]"})
49
+ _MUTATING_METHODS = frozenset({"POST", "PUT", "DELETE", "PATCH"})
50
+
51
+
52
+ def _hostname(value: str | None) -> str:
53
+ """Extract a bare, lowercased hostname from a Host header or Origin/Referer URL.
54
+
55
+ Handles values with or without a scheme and with or without a port, including
56
+ IPv6 literals like ``[::1]:3333``.
57
+ """
58
+ if not value:
59
+ return ""
60
+ value = value.strip()
61
+ # A bare Host header ("localhost:3333") has no scheme; prefix "//" so urlsplit
62
+ # parses it as a netloc rather than a path.
63
+ if "://" not in value:
64
+ value = "//" + value
65
+ try:
66
+ host = urlsplit(value).hostname or ""
67
+ except ValueError:
68
+ return ""
69
+ return host.lower()
70
+
71
+
72
+ def allowed_hostnames(bind_host: str | None = None,
73
+ extra: Iterable[str] | None = None) -> set[str]:
74
+ """Build the set of acceptable hostnames for this server.
75
+
76
+ Always includes loopback names. ``bind_host`` (unless it is the wildcard
77
+ ``0.0.0.0`` or empty) and any ``extra`` hosts are added so an intentional
78
+ non-loopback deployment remains reachable.
79
+ """
80
+ hosts = set(_LOOPBACK_HOSTS)
81
+ bh = (bind_host or "").strip().lower()
82
+ if bh and bh not in ("0.0.0.0", "::", "*"):
83
+ hosts.add(bh)
84
+ if extra:
85
+ for h in extra:
86
+ h = (h or "").strip().lower()
87
+ if h:
88
+ hosts.add(h)
89
+ return hosts
90
+
91
+
92
+ def check_request(request, allowed: set[str]) -> tuple[bool, str]:
93
+ """Return ``(ok, reason)``. ``ok == False`` means the request must be 403'd.
94
+
95
+ ``request`` is a Flask request (anything exposing ``.host``, ``.method`` and
96
+ ``.headers.get``).
97
+ """
98
+ # 1) Host-header allowlist — anti DNS-rebinding.
99
+ host = _hostname(getattr(request, "host", "") or "")
100
+ if host and host not in allowed:
101
+ return False, f"host '{host}' is not allowlisted"
102
+
103
+ # 2) Origin check — anti cross-origin CSRF. Browsers always send Origin on
104
+ # state-changing requests, so a mismatch is a reliable CSRF signal. When
105
+ # Origin is absent (typical for curl / API clients), fall back to Referer
106
+ # only for mutating methods; a fully header-less request is treated as a
107
+ # non-browser caller and allowed (it cannot be a CSRF from a page).
108
+ origin = request.headers.get("Origin")
109
+ if origin:
110
+ if _hostname(origin) not in allowed:
111
+ return False, "cross-origin request blocked (Origin)"
112
+ elif request.method in _MUTATING_METHODS:
113
+ referer = request.headers.get("Referer")
114
+ if referer and _hostname(referer) not in allowed:
115
+ return False, "cross-origin request blocked (Referer)"
116
+ return True, ""
117
+
118
+
119
+ def cors_origin(request, allowed: set[str]) -> str | None:
120
+ """Echo the request Origin in Access-Control-Allow-Origin only if it is
121
+ same-origin/allowlisted. The wildcard ``*`` is never used.
122
+ """
123
+ origin = request.headers.get("Origin")
124
+ if origin and _hostname(origin) in allowed:
125
+ return origin
126
+ return None
127
+
128
+
129
+ def install_guard(app, get_allowed: Callable[[], set[str]]) -> None:
130
+ """Register the Host/Origin guard and a tightened CORS policy on a Flask app.
131
+
132
+ ``get_allowed`` is called per-request so live config changes (e.g. a hub
133
+ ``host`` edit or an ``allowed_hosts`` list) are honoured without a restart.
134
+ Registering this BEFORE any other ``before_request`` (e.g. a bearer-token
135
+ guard) ensures cross-origin requests are rejected first.
136
+ """
137
+ from flask import jsonify, request
138
+
139
+ @app.before_request
140
+ def _c3_security_guard(): # noqa: ANN202 - Flask hook
141
+ # Let CORS preflight through; the after_request handler answers it and
142
+ # only reflects an allowlisted Origin, so disallowed origins still fail.
143
+ if request.method == "OPTIONS":
144
+ return None
145
+ ok, reason = check_request(request, get_allowed())
146
+ if not ok:
147
+ return jsonify({"error": f"blocked: {reason}"}), 403
148
+ return None
149
+
150
+ @app.after_request
151
+ def _c3_cors(response): # noqa: ANN202 - Flask hook
152
+ origin = cors_origin(request, get_allowed())
153
+ if origin:
154
+ response.headers["Access-Control-Allow-Origin"] = origin
155
+ response.headers["Vary"] = "Origin"
156
+ response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization"
157
+ response.headers["Access-Control-Allow-Methods"] = "GET,POST,PUT,DELETE,OPTIONS"
158
+ return response
@@ -23,12 +23,19 @@ from oracle.services import api_auth
23
23
  logger = logging.getLogger("oracle.mcp")
24
24
 
25
25
  _INSTRUCTIONS = (
26
- "C3 Oracle Discovery — cross-project code & memory intelligence as tools. "
27
- "Start with list_projects to see available projects, then use c3_search_cross "
28
- "or search_facts to discover across all of them, or the per-project tools "
29
- "(c3_search, c3_read, c3_compress, query_memory, read_graph) with a project_path. "
30
- "suggest_action creates a PENDING suggestion for human approval; delegate_task runs "
31
- "a configured Oracle agent. No code-editing tools are exposed."
26
+ "C3 Oracle Discovery — use C3's cross-project code & memory intelligence as tools.\n"
27
+ "\n"
28
+ "Recommended workflow:\n"
29
+ "1. list_projects see which C3 projects exist (names + absolute paths).\n"
30
+ "2. Discover across ALL projects: search_facts (memory) or c3_search_cross (code).\n"
31
+ "3. Narrow to one project using its path: c3_search to find code; c3_compress "
32
+ "(mode='map') to see a file's shape before reading; c3_read for exact content; "
33
+ "query_memory / read_graph / cross_insights for that project's memory.\n"
34
+ "\n"
35
+ "Notes: per-project tools REQUIRE a `project_path` taken from list_projects. Every tool "
36
+ "returns JSON. suggest_action creates a PENDING suggestion for a human to approve (not a "
37
+ "direct write); delegate_task runs a configured Oracle agent. Read + safe-action tiers "
38
+ "only — no code-editing tools are exposed."
32
39
  )
33
40
 
34
41