code-context-control 2.33.0__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.33.0 → code_context_control-2.34.0}/PKG-INFO +2 -2
  2. {code_context_control-2.33.0 → code_context_control-2.34.0}/README.md +1 -1
  3. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/c3.py +1 -1
  4. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/hub_server.py +6 -107
  5. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/server.py +14 -108
  6. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/tools/shell.py +59 -3
  7. {code_context_control-2.33.0 → code_context_control-2.34.0}/code_context_control.egg-info/PKG-INFO +2 -2
  8. {code_context_control-2.33.0 → code_context_control-2.34.0}/code_context_control.egg-info/SOURCES.txt +4 -0
  9. code_context_control-2.34.0/core/mcp_toml.py +128 -0
  10. {code_context_control-2.33.0 → code_context_control-2.34.0}/core/web_security.py +16 -0
  11. {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/mcp_oracle.py +43 -5
  12. {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/oracle_server.py +1 -0
  13. {code_context_control-2.33.0 → code_context_control-2.34.0}/pyproject.toml +1 -1
  14. code_context_control-2.34.0/tests/test_mcp_host_guard.py +52 -0
  15. code_context_control-2.34.0/tests/test_mcp_toml.py +97 -0
  16. code_context_control-2.34.0/tests/test_shell_robustness.py +90 -0
  17. {code_context_control-2.33.0 → code_context_control-2.34.0}/LICENSE +0 -0
  18. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/__init__.py +0 -0
  19. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/_hook_utils.py +0 -0
  20. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/commands/__init__.py +0 -0
  21. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/commands/common.py +0 -0
  22. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/commands/parser.py +0 -0
  23. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/docs.html +0 -0
  24. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/edits.html +0 -0
  25. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/hook_auto_snapshot.py +0 -0
  26. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/hook_c3_signal.py +0 -0
  27. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/hook_c3read.py +0 -0
  28. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/hook_edit_ledger.py +0 -0
  29. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/hook_edit_unlock.py +0 -0
  30. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/hook_filter.py +0 -0
  31. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/hook_ghost_files.py +0 -0
  32. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/hook_pretool_enforce.py +0 -0
  33. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/hook_read.py +0 -0
  34. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/hook_session_stats.py +0 -0
  35. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/hook_terse_advisor.py +0 -0
  36. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/hub.html +0 -0
  37. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/mcp_proxy.py +0 -0
  38. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/mcp_server.py +0 -0
  39. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/tools/__init__.py +0 -0
  40. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/tools/_helpers.py +0 -0
  41. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/tools/agent.py +0 -0
  42. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/tools/bitbucket.py +0 -0
  43. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/tools/compress.py +0 -0
  44. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/tools/delegate.py +0 -0
  45. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/tools/edit.py +0 -0
  46. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/tools/edits.py +0 -0
  47. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/tools/filter.py +0 -0
  48. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/tools/impact.py +0 -0
  49. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/tools/memory.py +0 -0
  50. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/tools/project.py +0 -0
  51. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/tools/read.py +0 -0
  52. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/tools/search.py +0 -0
  53. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/tools/session.py +0 -0
  54. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/tools/status.py +0 -0
  55. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/tools/validate.py +0 -0
  56. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/ui/api.js +0 -0
  57. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/ui/app.js +0 -0
  58. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/ui/components/bitbucket.js +0 -0
  59. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/ui/components/chat.js +0 -0
  60. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/ui/components/dashboard.js +0 -0
  61. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/ui/components/edits.js +0 -0
  62. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/ui/components/instructions.js +0 -0
  63. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/ui/components/memory.js +0 -0
  64. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/ui/components/sessions.js +0 -0
  65. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/ui/components/settings.js +0 -0
  66. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/ui/components/sidebar.js +0 -0
  67. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/ui/icons.js +0 -0
  68. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/ui/shared.js +0 -0
  69. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/ui/theme.js +0 -0
  70. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/ui.html +0 -0
  71. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/ui_legacy.html +0 -0
  72. {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/ui_nano.html +0 -0
  73. {code_context_control-2.33.0 → code_context_control-2.34.0}/code_context_control.egg-info/dependency_links.txt +0 -0
  74. {code_context_control-2.33.0 → code_context_control-2.34.0}/code_context_control.egg-info/entry_points.txt +0 -0
  75. {code_context_control-2.33.0 → code_context_control-2.34.0}/code_context_control.egg-info/requires.txt +0 -0
  76. {code_context_control-2.33.0 → code_context_control-2.34.0}/code_context_control.egg-info/top_level.txt +0 -0
  77. {code_context_control-2.33.0 → code_context_control-2.34.0}/core/__init__.py +0 -0
  78. {code_context_control-2.33.0 → code_context_control-2.34.0}/core/config.py +0 -0
  79. {code_context_control-2.33.0 → code_context_control-2.34.0}/core/ide.py +0 -0
  80. {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/__init__.py +0 -0
  81. {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/config.py +0 -0
  82. {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/oracle.html +0 -0
  83. {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/services/__init__.py +0 -0
  84. {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/services/api_auth.py +0 -0
  85. {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/services/c3_bridge.py +0 -0
  86. {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/services/chat_engine.py +0 -0
  87. {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/services/chat_store.py +0 -0
  88. {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/services/cross_memory.py +0 -0
  89. {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/services/federated_graph.py +0 -0
  90. {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/services/health_checker.py +0 -0
  91. {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/services/insight_engine.py +0 -0
  92. {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/services/memory_reader.py +0 -0
  93. {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/services/memory_writer.py +0 -0
  94. {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/services/ollama_bridge.py +0 -0
  95. {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/services/project_scanner.py +0 -0
  96. {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/services/review_agent.py +0 -0
  97. {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/services/tool_executor.py +0 -0
  98. {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/services/tool_registry.py +0 -0
  99. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/__init__.py +0 -0
  100. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/activity_log.py +0 -0
  101. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/agent_base.py +0 -0
  102. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/agents.py +0 -0
  103. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/auto_memory.py +0 -0
  104. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/bench/__init__.py +0 -0
  105. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/bench/external/__init__.py +0 -0
  106. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/bench/external/aider_polyglot.py +0 -0
  107. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/bench/external/swe_bench.py +0 -0
  108. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/benchmark_dashboard.py +0 -0
  109. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/bitbucket_client.py +0 -0
  110. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/bitbucket_credentials.py +0 -0
  111. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/claude_md.py +0 -0
  112. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/compressor.py +0 -0
  113. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/context_snapshot.py +0 -0
  114. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/conversation_store.py +0 -0
  115. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/doc_index.py +0 -0
  116. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/e2e_benchmark.py +0 -0
  117. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/e2e_evaluator.py +0 -0
  118. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/e2e_tasks.py +0 -0
  119. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/edit_ledger.py +0 -0
  120. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/embedding_index.py +0 -0
  121. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/error_reporting.py +0 -0
  122. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/file_memory.py +0 -0
  123. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/hub_service.py +0 -0
  124. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/indexer.py +0 -0
  125. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/memory.py +0 -0
  126. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/memory_consolidator.py +0 -0
  127. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/memory_graph.py +0 -0
  128. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/memory_grounder.py +0 -0
  129. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/memory_scorer.py +0 -0
  130. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/metrics.py +0 -0
  131. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/notifications.py +0 -0
  132. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/ollama_client.py +0 -0
  133. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/output_filter.py +0 -0
  134. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/parser.py +0 -0
  135. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/project_manager.py +0 -0
  136. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/project_runtime.py +0 -0
  137. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/protocol.py +0 -0
  138. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/proxy_state.py +0 -0
  139. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/retrieval_broker.py +0 -0
  140. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/router.py +0 -0
  141. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/runtime.py +0 -0
  142. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/session_benchmark.py +0 -0
  143. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/session_manager.py +0 -0
  144. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/session_preloader.py +0 -0
  145. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/text_index.py +0 -0
  146. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/tool_classifier.py +0 -0
  147. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/transcript_index.py +0 -0
  148. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/validation_cache.py +0 -0
  149. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/vector_store.py +0 -0
  150. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/version_tracker.py +0 -0
  151. {code_context_control-2.33.0 → code_context_control-2.34.0}/services/watcher.py +0 -0
  152. {code_context_control-2.33.0 → code_context_control-2.34.0}/setup.cfg +0 -0
  153. {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_aider_polyglot.py +0 -0
  154. {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_bitbucket_cli_smoke.py +0 -0
  155. {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_bitbucket_client.py +0 -0
  156. {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_bitbucket_credentials.py +0 -0
  157. {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_bitbucket_tool.py +0 -0
  158. {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_c3_shell.py +0 -0
  159. {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_cli_smoke.py +0 -0
  160. {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_e2e_benchmark.py +0 -0
  161. {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_edit_normalization.py +0 -0
  162. {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_enforcement_flip.py +0 -0
  163. {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_federated_graph.py +0 -0
  164. {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_ghost_files.py +0 -0
  165. {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_hub_server_smoke.py +0 -0
  166. {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_mcp_server_smoke.py +0 -0
  167. {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_memory_graph_api.py +0 -0
  168. {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_memory_system.py +0 -0
  169. {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_notification_discipline.py +0 -0
  170. {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_oracle_api_auth.py +0 -0
  171. {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_oracle_apikey_api.py +0 -0
  172. {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_oracle_discovery_api.py +0 -0
  173. {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_output_filter.py +0 -0
  174. {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_permissions.py +0 -0
  175. {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_project_manager.py +0 -0
  176. {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_project_manager_merge.py +0 -0
  177. {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_project_tool.py +0 -0
  178. {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_read_coercion.py +0 -0
  179. {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_session_benchmark.py +0 -0
  180. {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_session_budget.py +0 -0
  181. {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_swe_bench.py +0 -0
  182. {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_tool_registry.py +0 -0
  183. {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_validate.py +0 -0
  184. {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_web_security.py +0 -0
  185. {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_windows_reliability.py +0 -0
  186. {code_context_control-2.33.0 → code_context_control-2.34.0}/tui/__init__.py +0 -0
  187. {code_context_control-2.33.0 → code_context_control-2.34.0}/tui/backend.py +0 -0
  188. {code_context_control-2.33.0 → code_context_control-2.34.0}/tui/main.py +0 -0
  189. {code_context_control-2.33.0 → code_context_control-2.34.0}/tui/screens/__init__.py +0 -0
  190. {code_context_control-2.33.0 → code_context_control-2.34.0}/tui/screens/benchmark_view.py +0 -0
  191. {code_context_control-2.33.0 → code_context_control-2.34.0}/tui/screens/claudemd_view.py +0 -0
  192. {code_context_control-2.33.0 → code_context_control-2.34.0}/tui/screens/compress_view.py +0 -0
  193. {code_context_control-2.33.0 → code_context_control-2.34.0}/tui/screens/index_view.py +0 -0
  194. {code_context_control-2.33.0 → code_context_control-2.34.0}/tui/screens/init_view.py +0 -0
  195. {code_context_control-2.33.0 → code_context_control-2.34.0}/tui/screens/mcp_view.py +0 -0
  196. {code_context_control-2.33.0 → code_context_control-2.34.0}/tui/screens/optimize_view.py +0 -0
  197. {code_context_control-2.33.0 → code_context_control-2.34.0}/tui/screens/pipe_view.py +0 -0
  198. {code_context_control-2.33.0 → code_context_control-2.34.0}/tui/screens/projects_view.py +0 -0
  199. {code_context_control-2.33.0 → code_context_control-2.34.0}/tui/screens/search_view.py +0 -0
  200. {code_context_control-2.33.0 → code_context_control-2.34.0}/tui/screens/session_view.py +0 -0
  201. {code_context_control-2.33.0 → code_context_control-2.34.0}/tui/screens/stats.py +0 -0
  202. {code_context_control-2.33.0 → code_context_control-2.34.0}/tui/screens/ui_view.py +0 -0
  203. {code_context_control-2.33.0 → 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.33.0
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
- - **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.
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
- - **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.
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.33.0"
88
+ __version__ = "2.34.0"
89
89
 
90
90
 
91
91
  def _command_deps() -> CommandDeps:
@@ -180,46 +180,12 @@ def _project_mcp_config_path(project_root: Path, profile) -> Path:
180
180
  return (Path.home() / profile.config_path) if profile.config_path_global else (project_root / profile.config_path)
181
181
 
182
182
 
183
- def _parse_toml_mcp_servers(content: str) -> dict:
184
- servers = {}
185
- current_server = None
186
-
187
- for raw in content.splitlines():
188
- line = raw.split("#", 1)[0].strip()
189
- if not line:
190
- continue
191
-
192
- if line.startswith("[") and line.endswith("]"):
193
- section = line[1:-1].strip()
194
- if section.startswith("mcp_servers."):
195
- current_server = section.split(".", 1)[1]
196
- servers.setdefault(current_server, {})
197
- else:
198
- current_server = None
199
- continue
200
-
201
- if not current_server or "=" not in line:
202
- continue
203
-
204
- key, value = line.split("=", 1)
205
- key = key.strip().strip('"')
206
- value = value.strip()
207
-
208
- if key == "args":
209
- servers[current_server]["args"] = re.findall(r"[\"']([^\"']*)[\"']", value)
210
- elif key in ("command", "type"):
211
- match = re.match(r"^[\"'](.*)[\"']$", value)
212
- servers[current_server][key] = match.group(1) if match else value
213
- elif key == "enabled":
214
- low = value.lower()
215
- if low.startswith("true"):
216
- servers[current_server]["enabled"] = True
217
- elif low.startswith("false"):
218
- servers[current_server]["enabled"] = False
219
- else:
220
- servers[current_server][key] = value
221
-
222
- 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
+ )
223
189
 
224
190
 
225
191
  def _read_project_mcp_servers_for_profile(profile, mcp_file: Path) -> tuple[dict, dict]:
@@ -238,73 +204,6 @@ def _read_project_mcp_servers_for_profile(profile, mcp_file: Path) -> tuple[dict
238
204
  return servers, raw_config
239
205
 
240
206
 
241
- def _toml_escape_str(value: str) -> str:
242
- return value.replace("\\", "/")
243
-
244
-
245
- def _upsert_toml_section(toml_path: Path, section: str, entries: dict) -> None:
246
- content = toml_path.read_text(encoding="utf-8") if toml_path.exists() else ""
247
- header = f"[{section}]"
248
-
249
- lines = content.splitlines()
250
- new_lines = []
251
- skip = False
252
- for line in lines:
253
- stripped = line.strip()
254
- if stripped == header:
255
- skip = True
256
- continue
257
- if skip and stripped.startswith("["):
258
- skip = False
259
- if not skip:
260
- new_lines.append(line)
261
-
262
- content = "\n".join(new_lines).rstrip()
263
- section_lines = [f"\n\n{header}"]
264
- for key, value in entries.items():
265
- if isinstance(value, list):
266
- items = ", ".join(f'"{_toml_escape_str(str(item))}"' for item in value)
267
- section_lines.append(f'{key} = [{items}]')
268
- elif isinstance(value, bool):
269
- section_lines.append(f'{key} = {"true" if value else "false"}')
270
- else:
271
- section_lines.append(f'{key} = "{_toml_escape_str(str(value))}"')
272
- section_lines.append("")
273
-
274
- toml_path.parent.mkdir(parents=True, exist_ok=True)
275
- toml_path.write_text(content + "\n".join(section_lines), encoding="utf-8")
276
-
277
-
278
- def _remove_toml_section(toml_path: Path, section: str) -> bool:
279
- if not toml_path.exists():
280
- return False
281
- content = toml_path.read_text(encoding="utf-8")
282
- header = f"[{section}]"
283
-
284
- lines = content.splitlines()
285
- new_lines = []
286
- skip = False
287
- removed = False
288
- for line in lines:
289
- stripped = line.strip()
290
- if stripped == header:
291
- skip = True
292
- removed = True
293
- continue
294
- if skip and stripped.startswith("["):
295
- skip = False
296
- if not skip:
297
- new_lines.append(line)
298
-
299
- if removed:
300
- remaining = "\n".join(new_lines).rstrip()
301
- if remaining:
302
- toml_path.write_text(remaining + "\n", encoding="utf-8")
303
- else:
304
- toml_path.unlink()
305
- return removed
306
-
307
-
308
207
  def _build_mcp_cli_capabilities() -> dict:
309
208
  return {
310
209
  "commands": [
@@ -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
@@ -174,6 +173,9 @@ atexit.register(_cleanup_runtime)
174
173
  from core.web_security import (
175
174
  allowed_hostnames as _allowed_hostnames,
176
175
  )
176
+ from core.web_security import (
177
+ guard_summary as _guard_summary,
178
+ )
177
179
  from core.web_security import (
178
180
  install_guard as _install_web_guard,
179
181
  )
@@ -376,7 +378,8 @@ def api_health():
376
378
  except Exception:
377
379
  pass
378
380
 
379
- 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()})
380
383
 
381
384
 
382
385
  # ─── API: Session Registry ───────────────────────────────
@@ -2421,47 +2424,15 @@ def api_proxy_tools():
2421
2424
 
2422
2425
 
2423
2426
  # ─── API: MCP Status ─────────────────────────────────────
2424
- def _parse_toml_mcp_servers(content: str) -> dict:
2425
- """Parse [mcp_servers.<name>] sections from TOML content."""
2426
- servers = {}
2427
- current_server = None
2428
-
2429
- for raw in content.splitlines():
2430
- line = raw.split("#", 1)[0].strip()
2431
- if not line:
2432
- continue
2433
-
2434
- if line.startswith("[") and line.endswith("]"):
2435
- section = line[1:-1].strip()
2436
- if section.startswith("mcp_servers."):
2437
- current_server = section.split(".", 1)[1]
2438
- servers.setdefault(current_server, {})
2439
- else:
2440
- current_server = None
2441
- continue
2442
-
2443
- if not current_server or "=" not in line:
2444
- continue
2445
-
2446
- key, value = line.split("=", 1)
2447
- key = key.strip()
2448
- value = value.strip()
2449
-
2450
- if key == "args":
2451
- servers[current_server]["args"] = re.findall(r"[\"']([^\"']*)[\"']", value)
2452
- elif key in ("command", "type"):
2453
- match = re.match(r"^[\"'](.*)[\"']$", value)
2454
- servers[current_server][key] = match.group(1) if match else value
2455
- elif key == "enabled":
2456
- low = value.lower()
2457
- if low.startswith("true"):
2458
- servers[current_server]["enabled"] = True
2459
- elif low.startswith("false"):
2460
- servers[current_server]["enabled"] = False
2461
- else:
2462
- servers[current_server][key] = value
2463
-
2464
- 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
+ )
2465
2436
 
2466
2437
 
2467
2438
  def _find_server_script(servers: dict) -> bool:
@@ -2474,71 +2445,6 @@ def _find_server_script(servers: dict) -> bool:
2474
2445
  return False
2475
2446
 
2476
2447
 
2477
- def _toml_escape_str(value: str) -> str:
2478
- return value.replace("\\", "/")
2479
-
2480
-
2481
- def _upsert_toml_section(toml_path: Path, section: str, entries: dict) -> None:
2482
- """Add or replace a dotted TOML section in-place."""
2483
- content = toml_path.read_text(encoding="utf-8") if toml_path.exists() else ""
2484
- header = f"[{section}]"
2485
-
2486
- lines = content.splitlines()
2487
- new_lines = []
2488
- skip = False
2489
- for line in lines:
2490
- stripped = line.strip()
2491
- if stripped == header:
2492
- skip = True
2493
- continue
2494
- if skip and stripped.startswith("["):
2495
- skip = False
2496
- if not skip:
2497
- new_lines.append(line)
2498
-
2499
- content = "\n".join(new_lines).rstrip()
2500
- section_lines = [f"\n\n{header}"]
2501
- for k, v in entries.items():
2502
- if isinstance(v, list):
2503
- items = ", ".join(f'"{_toml_escape_str(str(x))}"' for x in v)
2504
- section_lines.append(f'{k} = [{items}]')
2505
- elif isinstance(v, bool):
2506
- section_lines.append(f'{k} = {"true" if v else "false"}')
2507
- else:
2508
- section_lines.append(f'{k} = "{_toml_escape_str(str(v))}"')
2509
- section_lines.append("")
2510
-
2511
- toml_path.parent.mkdir(parents=True, exist_ok=True)
2512
- toml_path.write_text(content + "\n".join(section_lines), encoding="utf-8")
2513
-
2514
-
2515
- def _remove_toml_section(toml_path: Path, section: str) -> bool:
2516
- """Remove a dotted TOML section. Returns True if removed."""
2517
- if not toml_path.exists():
2518
- return False
2519
- content = toml_path.read_text(encoding="utf-8")
2520
- header = f"[{section}]"
2521
-
2522
- lines = content.splitlines()
2523
- new_lines = []
2524
- skip = False
2525
- removed = False
2526
- for line in lines:
2527
- stripped = line.strip()
2528
- if stripped == header:
2529
- skip = True
2530
- removed = True
2531
- continue
2532
- if skip and stripped.startswith("["):
2533
- skip = False
2534
- if not skip:
2535
- new_lines.append(line)
2536
-
2537
- if removed:
2538
- toml_path.write_text("\n".join(new_lines).rstrip() + "\n", encoding="utf-8")
2539
- return removed
2540
-
2541
-
2542
2448
  def _resolve_mcp_profile(ide_name: str | None):
2543
2449
  requested = (ide_name or "").strip().lower()
2544
2450
  if requested and requested != "auto":
@@ -60,7 +60,13 @@ _FILTER_THRESHOLD_LINES = 30
60
60
 
61
61
 
62
62
  def _popen_kwargs() -> dict:
63
- kw: dict = {"stdin": subprocess.DEVNULL}
63
+ # Force UTF-8 in child processes so Unicode output (→, box-drawing, emoji)
64
+ # doesn't crash on Windows' legacy cp1252 console encoding. setdefault so an
65
+ # intentional caller-set encoding still wins.
66
+ env = dict(os.environ)
67
+ env.setdefault("PYTHONUTF8", "1")
68
+ env.setdefault("PYTHONIOENCODING", "utf-8")
69
+ kw: dict = {"stdin": subprocess.DEVNULL, "env": env}
64
70
  if sys.platform == "win32":
65
71
  kw["creationflags"] = subprocess.CREATE_NO_WINDOW
66
72
  return kw
@@ -86,7 +92,7 @@ def _run_sync(cmd: str, cwd: str, timeout: int) -> dict:
86
92
  proc = subprocess.Popen(
87
93
  cmd, shell=True, cwd=cwd,
88
94
  stdout=subprocess.PIPE, stderr=subprocess.PIPE,
89
- text=True, errors="replace",
95
+ text=True, encoding="utf-8", errors="replace",
90
96
  **_popen_kwargs(),
91
97
  )
92
98
  timed_out = False
@@ -133,6 +139,45 @@ def _maybe_refresh_ledger(cmd: str, result: dict, svc) -> list[str]:
133
139
  return []
134
140
 
135
141
 
142
+ # git diagnostics whose output the caller almost always needs verbatim — never
143
+ # auto-filter these, even past the line threshold.
144
+ _GIT_DIAGNOSTIC = re.compile(
145
+ r"^\s*git\s+(status|diff|log|show|branch|stash\s+list)\b", re.IGNORECASE
146
+ )
147
+
148
+
149
+ def _list_root_files(root: Path) -> set[str]:
150
+ try:
151
+ return {e.name for e in root.iterdir() if e.is_file()}
152
+ except OSError:
153
+ return set()
154
+
155
+
156
+ def _sweep_new_ghost_files(root: Path, before: set[str]) -> list[str]:
157
+ """Delete 0-byte 'ghost' files (shell-redirect / metacharacter artifacts —
158
+ e.g. a `>Lnnn` marker or `2>$null` leaking a filename) that appeared in
159
+ *root* during this command. Only files absent from *before* are removed, so
160
+ pre-existing files are never touched. Detection is reused from
161
+ hook_ghost_files so the rules live in one place; this makes c3_shell
162
+ self-clean regardless of whether the external PostToolUse ghost hook is
163
+ wired for this tool."""
164
+ try:
165
+ from cli.hook_ghost_files import scan_ghost_files
166
+ except Exception:
167
+ return []
168
+ swept: list[str] = []
169
+ for g in scan_ghost_files(root):
170
+ name = g.get("name", "")
171
+ if not name or name in before:
172
+ continue
173
+ try:
174
+ Path(g["path"]).unlink()
175
+ swept.append(name)
176
+ except OSError:
177
+ pass
178
+ return swept
179
+
180
+
136
181
  async def handle_shell(cmd: str, cwd: str, timeout: int, filter_output: bool,
137
182
  log: bool, svc, finalize) -> str:
138
183
  if not cmd or not cmd.strip():
@@ -147,11 +192,17 @@ async def handle_shell(cmd: str, cwd: str, timeout: int, filter_output: bool,
147
192
  work_cwd = cwd or svc.project_path
148
193
  work_cwd = str(Path(work_cwd).resolve())
149
194
 
195
+ ghost_root = Path(work_cwd)
196
+ _ghosts_before = _list_root_files(ghost_root)
197
+
150
198
  result = await asyncio.to_thread(_run_sync, cmd, work_cwd, timeout)
151
199
 
200
+ swept_ghosts = _sweep_new_ghost_files(ghost_root, _ghosts_before)
201
+
152
202
  raw_stdout = result["stdout"]
153
203
  filtered_note = ""
154
- if filter_output and raw_stdout.count("\n") > _FILTER_THRESHOLD_LINES:
204
+ if (filter_output and raw_stdout.count("\n") > _FILTER_THRESHOLD_LINES
205
+ and not _GIT_DIAGNOSTIC.search(cmd)):
155
206
  try:
156
207
  filtered = await asyncio.to_thread(
157
208
  handle_filter,
@@ -201,6 +252,11 @@ async def handle_shell(cmd: str, cwd: str, timeout: int, filter_output: bool,
201
252
  body += f"--- stderr ---\n{result['stderr'].rstrip()}\n"
202
253
  if touched_files:
203
254
  body += f"--- ledger ---\nlogged {len(touched_files)} file(s)\n"
255
+ if swept_ghosts:
256
+ body += (
257
+ f"--- ghost-sweep ---\nremoved {len(swept_ghosts)} stray 0-byte "
258
+ f"file(s): {', '.join(swept_ghosts)}\n"
259
+ )
204
260
 
205
261
  summary = f"shell {status} in {result['duration_ms']}ms"
206
262
  resp_tokens = count_tokens(body) if body else 0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-context-control
3
- Version: 2.33.0
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
- - **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.
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.
@@ -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/mcp_toml.py
72
73
  core/web_security.py
73
74
  oracle/__init__.py
74
75
  oracle/config.py
@@ -157,7 +158,9 @@ tests/test_enforcement_flip.py
157
158
  tests/test_federated_graph.py
158
159
  tests/test_ghost_files.py
159
160
  tests/test_hub_server_smoke.py
161
+ tests/test_mcp_host_guard.py
160
162
  tests/test_mcp_server_smoke.py
163
+ tests/test_mcp_toml.py
161
164
  tests/test_memory_graph_api.py
162
165
  tests/test_memory_system.py
163
166
  tests/test_notification_discipline.py
@@ -172,6 +175,7 @@ tests/test_project_tool.py
172
175
  tests/test_read_coercion.py
173
176
  tests/test_session_benchmark.py
174
177
  tests/test_session_budget.py
178
+ tests/test_shell_robustness.py
175
179
  tests/test_swe_bench.py
176
180
  tests/test_tool_registry.py
177
181
  tests/test_validate.py
@@ -0,0 +1,128 @@
1
+ """Shared TOML helpers for the MCP-server sections of IDE config files
2
+ (Codex's ``config.toml``, etc.).
3
+
4
+ These were duplicated — and had quietly drifted — between ``cli/server.py`` and
5
+ ``cli/hub_server.py``. Consolidating them keeps parse/write behaviour in one
6
+ place (the same triplication pattern that once let a CORS bug live in three
7
+ servers). The reconciled versions adopt the more robust behaviour from each
8
+ copy: ``parse`` strips surrounding quotes from keys, and ``remove`` deletes a
9
+ file that becomes empty instead of leaving an empty stub.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ from pathlib import Path
15
+
16
+
17
+ def parse_toml_mcp_servers(content: str) -> dict:
18
+ """Parse ``[mcp_servers.<name>]`` sections from TOML content into a dict."""
19
+ servers: dict = {}
20
+ current_server = None
21
+
22
+ for raw in content.splitlines():
23
+ line = raw.split("#", 1)[0].strip()
24
+ if not line:
25
+ continue
26
+
27
+ if line.startswith("[") and line.endswith("]"):
28
+ section = line[1:-1].strip()
29
+ if section.startswith("mcp_servers."):
30
+ current_server = section.split(".", 1)[1]
31
+ servers.setdefault(current_server, {})
32
+ else:
33
+ current_server = None
34
+ continue
35
+
36
+ if not current_server or "=" not in line:
37
+ continue
38
+
39
+ key, value = line.split("=", 1)
40
+ key = key.strip().strip('"')
41
+ value = value.strip()
42
+
43
+ if key == "args":
44
+ servers[current_server]["args"] = re.findall(r"[\"']([^\"']*)[\"']", value)
45
+ elif key in ("command", "type"):
46
+ match = re.match(r"^[\"'](.*)[\"']$", value)
47
+ servers[current_server][key] = match.group(1) if match else value
48
+ elif key == "enabled":
49
+ low = value.lower()
50
+ if low.startswith("true"):
51
+ servers[current_server]["enabled"] = True
52
+ elif low.startswith("false"):
53
+ servers[current_server]["enabled"] = False
54
+ else:
55
+ servers[current_server][key] = value
56
+
57
+ return servers
58
+
59
+
60
+ def toml_escape_str(value: str) -> str:
61
+ """Escape a string for a double-quoted TOML value (Windows ``\\`` → ``/``)."""
62
+ return value.replace("\\", "/")
63
+
64
+
65
+ def upsert_toml_section(toml_path: Path, section: str, entries: dict) -> None:
66
+ """Add or replace a dotted TOML section in-place."""
67
+ content = toml_path.read_text(encoding="utf-8") if toml_path.exists() else ""
68
+ header = f"[{section}]"
69
+
70
+ lines = content.splitlines()
71
+ new_lines = []
72
+ skip = False
73
+ for line in lines:
74
+ stripped = line.strip()
75
+ if stripped == header:
76
+ skip = True
77
+ continue
78
+ if skip and stripped.startswith("["):
79
+ skip = False
80
+ if not skip:
81
+ new_lines.append(line)
82
+
83
+ content = "\n".join(new_lines).rstrip()
84
+ section_lines = [f"\n\n{header}"]
85
+ for key, value in entries.items():
86
+ if isinstance(value, list):
87
+ items = ", ".join(f'"{toml_escape_str(str(item))}"' for item in value)
88
+ section_lines.append(f"{key} = [{items}]")
89
+ elif isinstance(value, bool):
90
+ section_lines.append(f'{key} = {"true" if value else "false"}')
91
+ else:
92
+ section_lines.append(f'{key} = "{toml_escape_str(str(value))}"')
93
+ section_lines.append("")
94
+
95
+ toml_path.parent.mkdir(parents=True, exist_ok=True)
96
+ toml_path.write_text(content + "\n".join(section_lines), encoding="utf-8")
97
+
98
+
99
+ def remove_toml_section(toml_path: Path, section: str) -> bool:
100
+ """Remove a dotted TOML section. Deletes the file if it becomes empty.
101
+ Returns True if the section was found and removed."""
102
+ if not toml_path.exists():
103
+ return False
104
+ content = toml_path.read_text(encoding="utf-8")
105
+ header = f"[{section}]"
106
+
107
+ lines = content.splitlines()
108
+ new_lines = []
109
+ skip = False
110
+ removed = False
111
+ for line in lines:
112
+ stripped = line.strip()
113
+ if stripped == header:
114
+ skip = True
115
+ removed = True
116
+ continue
117
+ if skip and stripped.startswith("["):
118
+ skip = False
119
+ if not skip:
120
+ new_lines.append(line)
121
+
122
+ if removed:
123
+ remaining = "\n".join(new_lines).rstrip()
124
+ if remaining:
125
+ toml_path.write_text(remaining + "\n", encoding="utf-8")
126
+ else:
127
+ toml_path.unlink()
128
+ return removed
@@ -126,6 +126,17 @@ def cors_origin(request, allowed: set[str]) -> str | None:
126
126
  return None
127
127
 
128
128
 
129
+ def guard_summary() -> dict:
130
+ """Compact, serializable status for health endpoints — confirms to operators
131
+ that the localhost guard is active (it otherwise enforces silently)."""
132
+ return {
133
+ "active": True,
134
+ "host_allowlist": True,
135
+ "csrf": "origin+referer",
136
+ "cors": "scoped",
137
+ }
138
+
139
+
129
140
  def install_guard(app, get_allowed: Callable[[], set[str]]) -> None:
130
141
  """Register the Host/Origin guard and a tightened CORS policy on a Flask app.
131
142
 
@@ -156,3 +167,8 @@ def install_guard(app, get_allowed: Callable[[], set[str]]) -> None:
156
167
  response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization"
157
168
  response.headers["Access-Control-Allow-Methods"] = "GET,POST,PUT,DELETE,OPTIONS"
158
169
  return response
170
+
171
+ import logging
172
+ logging.getLogger("c3.web_security").info(
173
+ "localhost web guard active — Host allowlist + Origin/Referer CSRF + scoped CORS"
174
+ )