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.
- {code_context_control-2.33.0 → code_context_control-2.34.0}/PKG-INFO +2 -2
- {code_context_control-2.33.0 → code_context_control-2.34.0}/README.md +1 -1
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/c3.py +1 -1
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/hub_server.py +6 -107
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/server.py +14 -108
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/tools/shell.py +59 -3
- {code_context_control-2.33.0 → code_context_control-2.34.0}/code_context_control.egg-info/PKG-INFO +2 -2
- {code_context_control-2.33.0 → code_context_control-2.34.0}/code_context_control.egg-info/SOURCES.txt +4 -0
- code_context_control-2.34.0/core/mcp_toml.py +128 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/core/web_security.py +16 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/mcp_oracle.py +43 -5
- {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/oracle_server.py +1 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/pyproject.toml +1 -1
- code_context_control-2.34.0/tests/test_mcp_host_guard.py +52 -0
- code_context_control-2.34.0/tests/test_mcp_toml.py +97 -0
- code_context_control-2.34.0/tests/test_shell_robustness.py +90 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/LICENSE +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/__init__.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/_hook_utils.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/commands/__init__.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/commands/common.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/commands/parser.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/docs.html +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/edits.html +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/hook_auto_snapshot.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/hook_c3_signal.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/hook_c3read.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/hook_edit_ledger.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/hook_edit_unlock.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/hook_filter.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/hook_ghost_files.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/hook_pretool_enforce.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/hook_read.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/hook_session_stats.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/hook_terse_advisor.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/hub.html +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/mcp_proxy.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/mcp_server.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/tools/__init__.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/tools/_helpers.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/tools/agent.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/tools/bitbucket.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/tools/compress.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/tools/delegate.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/tools/edit.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/tools/edits.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/tools/filter.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/tools/impact.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/tools/memory.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/tools/project.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/tools/read.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/tools/search.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/tools/session.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/tools/status.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/tools/validate.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/ui/api.js +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/ui/app.js +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/ui/components/bitbucket.js +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/ui/components/chat.js +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/ui/components/dashboard.js +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/ui/components/edits.js +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/ui/components/instructions.js +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/ui/components/memory.js +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/ui/components/sessions.js +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/ui/components/settings.js +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/ui/components/sidebar.js +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/ui/icons.js +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/ui/shared.js +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/ui/theme.js +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/ui.html +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/ui_legacy.html +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/cli/ui_nano.html +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/code_context_control.egg-info/dependency_links.txt +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/code_context_control.egg-info/entry_points.txt +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/code_context_control.egg-info/requires.txt +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/code_context_control.egg-info/top_level.txt +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/core/__init__.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/core/config.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/core/ide.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/__init__.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/config.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/oracle.html +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/services/__init__.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/services/api_auth.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/services/c3_bridge.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/services/chat_engine.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/services/chat_store.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/services/cross_memory.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/services/federated_graph.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/services/health_checker.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/services/insight_engine.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/services/memory_reader.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/services/memory_writer.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/services/ollama_bridge.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/services/project_scanner.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/services/review_agent.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/services/tool_executor.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/oracle/services/tool_registry.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/__init__.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/activity_log.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/agent_base.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/agents.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/auto_memory.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/bench/__init__.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/bench/external/__init__.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/bench/external/aider_polyglot.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/bench/external/swe_bench.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/benchmark_dashboard.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/bitbucket_client.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/bitbucket_credentials.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/claude_md.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/compressor.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/context_snapshot.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/conversation_store.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/doc_index.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/e2e_benchmark.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/e2e_evaluator.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/e2e_tasks.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/edit_ledger.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/embedding_index.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/error_reporting.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/file_memory.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/hub_service.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/indexer.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/memory.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/memory_consolidator.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/memory_graph.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/memory_grounder.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/memory_scorer.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/metrics.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/notifications.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/ollama_client.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/output_filter.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/parser.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/project_manager.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/project_runtime.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/protocol.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/proxy_state.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/retrieval_broker.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/router.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/runtime.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/session_benchmark.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/session_manager.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/session_preloader.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/text_index.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/tool_classifier.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/transcript_index.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/validation_cache.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/vector_store.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/version_tracker.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/services/watcher.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/setup.cfg +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_aider_polyglot.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_bitbucket_cli_smoke.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_bitbucket_client.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_bitbucket_credentials.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_bitbucket_tool.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_c3_shell.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_cli_smoke.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_e2e_benchmark.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_edit_normalization.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_enforcement_flip.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_federated_graph.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_ghost_files.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_hub_server_smoke.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_mcp_server_smoke.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_memory_graph_api.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_memory_system.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_notification_discipline.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_oracle_api_auth.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_oracle_apikey_api.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_oracle_discovery_api.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_output_filter.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_permissions.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_project_manager.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_project_manager_merge.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_project_tool.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_read_coercion.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_session_benchmark.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_session_budget.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_swe_bench.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_tool_registry.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_validate.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_web_security.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tests/test_windows_reliability.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tui/__init__.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tui/backend.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tui/main.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tui/screens/__init__.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tui/screens/benchmark_view.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tui/screens/claudemd_view.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tui/screens/compress_view.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tui/screens/index_view.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tui/screens/init_view.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tui/screens/mcp_view.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tui/screens/optimize_view.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tui/screens/pipe_view.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tui/screens/projects_view.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tui/screens/search_view.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tui/screens/session_view.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tui/screens/stats.py +0 -0
- {code_context_control-2.33.0 → code_context_control-2.34.0}/tui/screens/ui_view.py +0 -0
- {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.
|
|
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.
|
|
@@ -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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
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
|
-
|
|
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
|
{code_context_control-2.33.0 → code_context_control-2.34.0}/code_context_control.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-context-control
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.34.0
|
|
4
4
|
Summary: Local code-intelligence layer for AI coding tools (Claude Code, Codex, Gemini, Copilot). Retrieve less, read less, edit safer.
|
|
5
5
|
Author-email: Dimitri Tselenchuk <dtselenc@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -363,7 +363,7 @@ Real-world A/B tests: same task, with and without C3 mounted. Reports include to
|
|
|
363
363
|
|
|
364
364
|
## Security & privacy
|
|
365
365
|
|
|
366
|
-
- **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
|
+
)
|