code-context-control 2.32.1__tar.gz → 2.33.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {code_context_control-2.32.1 → code_context_control-2.33.0}/PKG-INFO +31 -2
- {code_context_control-2.32.1 → code_context_control-2.33.0}/README.md +30 -1
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/c3.py +21 -4
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/hook_ghost_files.py +12 -2
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/hub_server.py +25 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/mcp_server.py +3 -1
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/server.py +17 -6
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/tools/filter.py +2 -2
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/tools/read.py +37 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/tools/shell.py +22 -2
- {code_context_control-2.32.1 → code_context_control-2.33.0}/code_context_control.egg-info/PKG-INFO +31 -2
- {code_context_control-2.32.1 → code_context_control-2.33.0}/code_context_control.egg-info/SOURCES.txt +3 -0
- code_context_control-2.33.0/core/web_security.py +158 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/mcp_oracle.py +13 -6
- {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/oracle_server.py +15 -6
- {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/services/tool_registry.py +17 -2
- {code_context_control-2.32.1 → code_context_control-2.33.0}/pyproject.toml +1 -1
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/claude_md.py +1 -1
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_c3_shell.py +34 -0
- code_context_control-2.33.0/tests/test_read_coercion.py +68 -0
- code_context_control-2.33.0/tests/test_web_security.py +106 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/LICENSE +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/__init__.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/_hook_utils.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/commands/__init__.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/commands/common.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/commands/parser.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/docs.html +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/edits.html +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/hook_auto_snapshot.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/hook_c3_signal.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/hook_c3read.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/hook_edit_ledger.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/hook_edit_unlock.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/hook_filter.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/hook_pretool_enforce.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/hook_read.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/hook_session_stats.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/hook_terse_advisor.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/hub.html +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/mcp_proxy.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/tools/__init__.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/tools/_helpers.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/tools/agent.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/tools/bitbucket.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/tools/compress.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/tools/delegate.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/tools/edit.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/tools/edits.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/tools/impact.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/tools/memory.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/tools/project.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/tools/search.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/tools/session.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/tools/status.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/tools/validate.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/ui/api.js +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/ui/app.js +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/ui/components/bitbucket.js +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/ui/components/chat.js +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/ui/components/dashboard.js +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/ui/components/edits.js +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/ui/components/instructions.js +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/ui/components/memory.js +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/ui/components/sessions.js +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/ui/components/settings.js +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/ui/components/sidebar.js +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/ui/icons.js +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/ui/shared.js +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/ui/theme.js +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/ui.html +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/ui_legacy.html +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/cli/ui_nano.html +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/code_context_control.egg-info/dependency_links.txt +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/code_context_control.egg-info/entry_points.txt +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/code_context_control.egg-info/requires.txt +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/code_context_control.egg-info/top_level.txt +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/core/__init__.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/core/config.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/core/ide.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/__init__.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/config.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/oracle.html +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/services/__init__.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/services/api_auth.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/services/c3_bridge.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/services/chat_engine.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/services/chat_store.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/services/cross_memory.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/services/federated_graph.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/services/health_checker.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/services/insight_engine.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/services/memory_reader.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/services/memory_writer.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/services/ollama_bridge.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/services/project_scanner.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/services/review_agent.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/oracle/services/tool_executor.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/__init__.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/activity_log.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/agent_base.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/agents.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/auto_memory.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/bench/__init__.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/bench/external/__init__.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/bench/external/aider_polyglot.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/bench/external/swe_bench.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/benchmark_dashboard.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/bitbucket_client.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/bitbucket_credentials.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/compressor.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/context_snapshot.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/conversation_store.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/doc_index.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/e2e_benchmark.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/e2e_evaluator.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/e2e_tasks.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/edit_ledger.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/embedding_index.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/error_reporting.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/file_memory.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/hub_service.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/indexer.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/memory.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/memory_consolidator.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/memory_graph.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/memory_grounder.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/memory_scorer.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/metrics.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/notifications.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/ollama_client.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/output_filter.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/parser.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/project_manager.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/project_runtime.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/protocol.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/proxy_state.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/retrieval_broker.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/router.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/runtime.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/session_benchmark.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/session_manager.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/session_preloader.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/text_index.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/tool_classifier.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/transcript_index.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/validation_cache.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/vector_store.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/version_tracker.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/services/watcher.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/setup.cfg +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_aider_polyglot.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_bitbucket_cli_smoke.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_bitbucket_client.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_bitbucket_credentials.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_bitbucket_tool.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_cli_smoke.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_e2e_benchmark.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_edit_normalization.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_enforcement_flip.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_federated_graph.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_ghost_files.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_hub_server_smoke.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_mcp_server_smoke.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_memory_graph_api.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_memory_system.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_notification_discipline.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_oracle_api_auth.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_oracle_apikey_api.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_oracle_discovery_api.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_output_filter.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_permissions.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_project_manager.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_project_manager_merge.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_project_tool.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_session_benchmark.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_session_budget.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_swe_bench.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_tool_registry.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_validate.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tests/test_windows_reliability.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tui/__init__.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tui/backend.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tui/main.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tui/screens/__init__.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tui/screens/benchmark_view.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tui/screens/claudemd_view.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tui/screens/compress_view.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tui/screens/index_view.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tui/screens/init_view.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tui/screens/mcp_view.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tui/screens/optimize_view.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tui/screens/pipe_view.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tui/screens/projects_view.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tui/screens/search_view.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tui/screens/session_view.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tui/screens/stats.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tui/screens/ui_view.py +0 -0
- {code_context_control-2.32.1 → code_context_control-2.33.0}/tui/theme.tcss +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-context-control
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.33.0
|
|
4
4
|
Summary: Local code-intelligence layer for AI coding tools (Claude Code, Codex, Gemini, Copilot). Retrieve less, read less, edit safer.
|
|
5
5
|
Author-email: Dimitri Tselenchuk <dtselenc@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -281,6 +281,34 @@ to the C3 edit ledger so the audit trail covers platform-side changes too.
|
|
|
281
281
|
The **Hub UI** (per-project) gains a "Bitbucket" tab with sub-views for
|
|
282
282
|
Overview / Pull Requests / Branches / Activity / Admin.
|
|
283
283
|
|
|
284
|
+
### Oracle Discovery API (v2.32.0)
|
|
285
|
+
|
|
286
|
+
The **Oracle** is C3's optional cross-project memory agent (a local web app). As of
|
|
287
|
+
v2.32.0 it can expose C3's cross-project code & memory intelligence as **tools for an
|
|
288
|
+
external LLM** — point Claude (or any function-calling model) at a running Oracle and
|
|
289
|
+
it can discover your projects and search code, memory, and the cross-project graph
|
|
290
|
+
across all of them.
|
|
291
|
+
|
|
292
|
+
Two transports share one tool core:
|
|
293
|
+
|
|
294
|
+
- **MCP** (streamable HTTP/SSE) at `http://127.0.0.1:3332/mcp` — native for Claude
|
|
295
|
+
Code / Claude Desktop / any MCP client.
|
|
296
|
+
- **OpenAPI REST** at `http://127.0.0.1:3331/api/discovery` — for any LLM with
|
|
297
|
+
function-calling (fetch `/openapi.json` to auto-register the tools).
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
# Start the Oracle (serves the REST + MCP discovery endpoints)
|
|
301
|
+
python oracle/oracle_server.py --no-browser
|
|
302
|
+
|
|
303
|
+
# Print the Bearer token + a ready-to-paste .mcp.json snippet
|
|
304
|
+
c3 oracle api info
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Only **read** and **safe-action** tools are exposed (no code editing); requests need a
|
|
308
|
+
**Bearer token** (stored in the OS keyring) and both servers bind `127.0.0.1` by
|
|
309
|
+
default. Generate, rotate, and copy the token from the dashboard's **Settings →
|
|
310
|
+
Discovery API** tab. See the [Oracle Discovery API guide](oracle-guide/discovery-api.md).
|
|
311
|
+
|
|
284
312
|
---
|
|
285
313
|
|
|
286
314
|
## Tiered local AI (optional)
|
|
@@ -335,7 +363,7 @@ Real-world A/B tests: same task, with and without C3 mounted. Reports include to
|
|
|
335
363
|
|
|
336
364
|
## Security & privacy
|
|
337
365
|
|
|
338
|
-
- **Hub
|
|
366
|
+
- **All web servers (Hub, per-project UI, Oracle) bind to `127.0.0.1` by default and are guarded against browser-based attacks even on loopback** — a Host-header allowlist (defeats DNS rebinding) plus an Origin/Referer check on every request (defeats cross-origin CSRF), with scoped, non-wildcard CORS. A malicious web page you visit therefore cannot drive C3's local endpoints. There is still **no user authentication**, so do not expose these servers to an untrusted network without auth/TLS in front. Binding to a non-loopback interface in `~/.c3/hub_config.json` (`host`) or Oracle's config (`bind_host`) is opt-in and warned at startup; add externally-facing hostnames/IPs to an `allowed_hosts` list there so the guard permits them.
|
|
339
367
|
- **No telemetry by default.** The OSS package collects nothing. Opt-in Sentry crash reporting requires the `[telemetry]` extra plus both `SENTRY_DSN` and `C3_TELEMETRY_OPT_IN=1`. Even when enabled, request bodies, local variables, and prompts are stripped before sending.
|
|
340
368
|
- **API keys** for third-party model providers are read from environment variables and never persisted by C3.
|
|
341
369
|
- See [`SECURITY.md`](SECURITY.md) for the full hardening guide and disclosure policy.
|
|
@@ -355,6 +383,7 @@ The author may introduce a paid offering or relicense future major versions; no
|
|
|
355
383
|
|
|
356
384
|
- **PyPI:** https://pypi.org/project/code-context-control/
|
|
357
385
|
- **Changelog:** [`CHANGELOG.md`](CHANGELOG.md)
|
|
386
|
+
- **Oracle Discovery API:** [`oracle-guide/discovery-api.md`](oracle-guide/discovery-api.md)
|
|
358
387
|
- **Security policy:** [`SECURITY.md`](SECURITY.md)
|
|
359
388
|
- **Licensing FAQ:** [`LICENSING.md`](LICENSING.md)
|
|
360
389
|
- **Issues:** https://github.com/drknowhow/code-context-control/issues
|
|
@@ -219,6 +219,34 @@ to the C3 edit ledger so the audit trail covers platform-side changes too.
|
|
|
219
219
|
The **Hub UI** (per-project) gains a "Bitbucket" tab with sub-views for
|
|
220
220
|
Overview / Pull Requests / Branches / Activity / Admin.
|
|
221
221
|
|
|
222
|
+
### Oracle Discovery API (v2.32.0)
|
|
223
|
+
|
|
224
|
+
The **Oracle** is C3's optional cross-project memory agent (a local web app). As of
|
|
225
|
+
v2.32.0 it can expose C3's cross-project code & memory intelligence as **tools for an
|
|
226
|
+
external LLM** — point Claude (or any function-calling model) at a running Oracle and
|
|
227
|
+
it can discover your projects and search code, memory, and the cross-project graph
|
|
228
|
+
across all of them.
|
|
229
|
+
|
|
230
|
+
Two transports share one tool core:
|
|
231
|
+
|
|
232
|
+
- **MCP** (streamable HTTP/SSE) at `http://127.0.0.1:3332/mcp` — native for Claude
|
|
233
|
+
Code / Claude Desktop / any MCP client.
|
|
234
|
+
- **OpenAPI REST** at `http://127.0.0.1:3331/api/discovery` — for any LLM with
|
|
235
|
+
function-calling (fetch `/openapi.json` to auto-register the tools).
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
# Start the Oracle (serves the REST + MCP discovery endpoints)
|
|
239
|
+
python oracle/oracle_server.py --no-browser
|
|
240
|
+
|
|
241
|
+
# Print the Bearer token + a ready-to-paste .mcp.json snippet
|
|
242
|
+
c3 oracle api info
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Only **read** and **safe-action** tools are exposed (no code editing); requests need a
|
|
246
|
+
**Bearer token** (stored in the OS keyring) and both servers bind `127.0.0.1` by
|
|
247
|
+
default. Generate, rotate, and copy the token from the dashboard's **Settings →
|
|
248
|
+
Discovery API** tab. See the [Oracle Discovery API guide](oracle-guide/discovery-api.md).
|
|
249
|
+
|
|
222
250
|
---
|
|
223
251
|
|
|
224
252
|
## Tiered local AI (optional)
|
|
@@ -273,7 +301,7 @@ Real-world A/B tests: same task, with and without C3 mounted. Reports include to
|
|
|
273
301
|
|
|
274
302
|
## Security & privacy
|
|
275
303
|
|
|
276
|
-
- **Hub
|
|
304
|
+
- **All web servers (Hub, per-project UI, Oracle) bind to `127.0.0.1` by default and are guarded against browser-based attacks even on loopback** — a Host-header allowlist (defeats DNS rebinding) plus an Origin/Referer check on every request (defeats cross-origin CSRF), with scoped, non-wildcard CORS. A malicious web page you visit therefore cannot drive C3's local endpoints. There is still **no user authentication**, so do not expose these servers to an untrusted network without auth/TLS in front. Binding to a non-loopback interface in `~/.c3/hub_config.json` (`host`) or Oracle's config (`bind_host`) is opt-in and warned at startup; add externally-facing hostnames/IPs to an `allowed_hosts` list there so the guard permits them.
|
|
277
305
|
- **No telemetry by default.** The OSS package collects nothing. Opt-in Sentry crash reporting requires the `[telemetry]` extra plus both `SENTRY_DSN` and `C3_TELEMETRY_OPT_IN=1`. Even when enabled, request bodies, local variables, and prompts are stripped before sending.
|
|
278
306
|
- **API keys** for third-party model providers are read from environment variables and never persisted by C3.
|
|
279
307
|
- See [`SECURITY.md`](SECURITY.md) for the full hardening guide and disclosure policy.
|
|
@@ -293,6 +321,7 @@ The author may introduce a paid offering or relicense future major versions; no
|
|
|
293
321
|
|
|
294
322
|
- **PyPI:** https://pypi.org/project/code-context-control/
|
|
295
323
|
- **Changelog:** [`CHANGELOG.md`](CHANGELOG.md)
|
|
324
|
+
- **Oracle Discovery API:** [`oracle-guide/discovery-api.md`](oracle-guide/discovery-api.md)
|
|
296
325
|
- **Security policy:** [`SECURITY.md`](SECURITY.md)
|
|
297
326
|
- **Licensing FAQ:** [`LICENSING.md`](LICENSING.md)
|
|
298
327
|
- **Issues:** https://github.com/drknowhow/code-context-control/issues
|
|
@@ -85,7 +85,7 @@ console = Console() if HAS_RICH else None
|
|
|
85
85
|
# Config
|
|
86
86
|
CONFIG_DIR = ".c3"
|
|
87
87
|
CONFIG_FILE = ".c3/config.json"
|
|
88
|
-
__version__ = "2.
|
|
88
|
+
__version__ = "2.33.0"
|
|
89
89
|
|
|
90
90
|
|
|
91
91
|
def _command_deps() -> CommandDeps:
|
|
@@ -4921,8 +4921,14 @@ def cmd_install_mcp(args):
|
|
|
4921
4921
|
# Build hook commands using the Python executable that runs c3.
|
|
4922
4922
|
# On Windows, Claude Code executes hooks via /usr/bin/bash (Git Bash), which cannot
|
|
4923
4923
|
# parse Windows absolute paths containing parentheses (e.g. "(C3)"). Prefix with
|
|
4924
|
-
#
|
|
4925
|
-
|
|
4924
|
+
# cmd.exe so it handles path resolution instead of bash.
|
|
4925
|
+
#
|
|
4926
|
+
# Use "cmd.exe" WITH the extension, not bare "cmd": Git Bash does not resolve bare
|
|
4927
|
+
# "cmd" on PATH, so the old "cmd /c …" prefix silently failed to launch any hook
|
|
4928
|
+
# (verified: under bash, "cmd.exe /c '<py>' '<hook>'" runs and writes the signal
|
|
4929
|
+
# file; "cmd /c …" returns "cmd: command not found"). The single-quoted paths are
|
|
4930
|
+
# correct — bash strips them and re-quotes for cmd.exe, preserving spaces/parens.
|
|
4931
|
+
_hook_prefix = "cmd.exe /c " if sys.platform == "win32" else ""
|
|
4926
4932
|
hook_filter_cmd = f"{_hook_prefix}{shlex.quote(sys.executable)} {shlex.quote(str(cli_dir / 'hook_filter.py'))}"
|
|
4927
4933
|
hook_read_cmd = f"{_hook_prefix}{shlex.quote(sys.executable)} {shlex.quote(str(cli_dir / 'hook_read.py'))}"
|
|
4928
4934
|
hook_c3read_cmd = f"{_hook_prefix}{shlex.quote(sys.executable)} {shlex.quote(str(cli_dir / 'hook_c3read.py'))}"
|
|
@@ -4962,13 +4968,24 @@ def cmd_install_mcp(args):
|
|
|
4962
4968
|
},
|
|
4963
4969
|
{
|
|
4964
4970
|
"matcher": read_matcher,
|
|
4965
|
-
"hooks": [
|
|
4971
|
+
"hooks": [
|
|
4972
|
+
{"type": "command", "command": hook_read_cmd},
|
|
4973
|
+
{"type": "command", "command": hook_ghost_files_cmd},
|
|
4974
|
+
]
|
|
4966
4975
|
},
|
|
4967
4976
|
{
|
|
4968
4977
|
"matcher": "mcp__c3__c3_read",
|
|
4969
4978
|
"hooks": [
|
|
4970
4979
|
{"type": "command", "command": hook_c3read_cmd},
|
|
4971
4980
|
{"type": "command", "command": hook_c3_signal_cmd},
|
|
4981
|
+
{"type": "command", "command": hook_ghost_files_cmd},
|
|
4982
|
+
]
|
|
4983
|
+
},
|
|
4984
|
+
{
|
|
4985
|
+
"matcher": "mcp__c3__c3_shell",
|
|
4986
|
+
"hooks": [
|
|
4987
|
+
{"type": "command", "command": hook_c3_signal_cmd},
|
|
4988
|
+
{"type": "command", "command": hook_ghost_files_cmd},
|
|
4972
4989
|
]
|
|
4973
4990
|
},
|
|
4974
4991
|
{
|
|
@@ -212,6 +212,17 @@ def cleanup_ghost_files(ghosts: list[dict]) -> list[str]:
|
|
|
212
212
|
return deleted
|
|
213
213
|
|
|
214
214
|
|
|
215
|
+
# Tools whose output can carry shell-meta text that leaks into 0-byte files:
|
|
216
|
+
# native shells, c3_shell (its `N->Mtok` filter header), and file reads whose
|
|
217
|
+
# content has `-> Type` hints. A downstream shell sees `> word` and creates an
|
|
218
|
+
# empty file named `word`.
|
|
219
|
+
_GHOST_TRIGGER_TOOLS = (
|
|
220
|
+
"Bash", "run_shell_command",
|
|
221
|
+
"mcp__c3__c3_shell",
|
|
222
|
+
"mcp__c3__c3_read", "Read", "read_file",
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
215
226
|
def main():
|
|
216
227
|
try:
|
|
217
228
|
raw = sys.stdin.read()
|
|
@@ -221,8 +232,7 @@ def main():
|
|
|
221
232
|
data = json.loads(raw)
|
|
222
233
|
tool_name = data.get("tool_name", "")
|
|
223
234
|
|
|
224
|
-
|
|
225
|
-
if tool_name not in ("Bash", "run_shell_command"):
|
|
235
|
+
if tool_name not in _GHOST_TRIGGER_TOOLS:
|
|
226
236
|
return
|
|
227
237
|
|
|
228
238
|
is_gemini = isinstance(data.get("tool_response", ""), dict)
|
|
@@ -35,6 +35,26 @@ from services.tool_classifier import CATEGORIES
|
|
|
35
35
|
|
|
36
36
|
app = Flask(__name__, static_folder=str(Path(__file__).parent))
|
|
37
37
|
|
|
38
|
+
# Localhost-only security: Host-header allowlist + Origin/Referer CSRF guard +
|
|
39
|
+
# scoped CORS. The hub manages MANY projects and exposes command-executing
|
|
40
|
+
# endpoints (launch-ide, mcp-server-add, permissions), so cross-origin CSRF /
|
|
41
|
+
# DNS-rebinding protection matters even though it binds loopback by default.
|
|
42
|
+
# Reads bind host + optional allowed_hosts per-request from hub_config.json.
|
|
43
|
+
from core.web_security import (
|
|
44
|
+
allowed_hostnames as _allowed_hostnames,
|
|
45
|
+
)
|
|
46
|
+
from core.web_security import (
|
|
47
|
+
install_guard as _install_web_guard,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _hub_allowed_hosts():
|
|
52
|
+
_c = _read_hub_config()
|
|
53
|
+
return _allowed_hostnames(_c.get("host"), _c.get("allowed_hosts"))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
_install_web_guard(app, _hub_allowed_hosts)
|
|
57
|
+
|
|
38
58
|
# ─── Hub config ───────────────────────────────────────────────────────────────
|
|
39
59
|
|
|
40
60
|
_GLOBAL_C3_DIR = Path.home() / ".c3"
|
|
@@ -519,6 +539,11 @@ def api_projects_open():
|
|
|
519
539
|
path = Path(path_str).resolve()
|
|
520
540
|
if not path.exists():
|
|
521
541
|
return jsonify({"error": f"Path does not exist: {path_str}"}), 404
|
|
542
|
+
# Only ever open directories. Opening a *file* via os.startfile would
|
|
543
|
+
# invoke its default handler (e.g. run an .exe/.bat/.lnk), so refuse
|
|
544
|
+
# anything that is not a folder.
|
|
545
|
+
if not path.is_dir():
|
|
546
|
+
return jsonify({"error": "Only directories can be opened"}), 400
|
|
522
547
|
|
|
523
548
|
if sys.platform == "win32":
|
|
524
549
|
os.startfile(str(path))
|
|
@@ -639,7 +639,9 @@ async def c3_shell(cmd: str, cwd: str = "", timeout: int = 60,
|
|
|
639
639
|
"""EXECUTE shell command — structured returns, auto-filter, ledger-aware.
|
|
640
640
|
Use for tests, git, build, scripts. Returns exit_code/stdout/stderr/duration_ms.
|
|
641
641
|
Auto-filters stdout >30 lines; auto-logs git mutations to the edit ledger.
|
|
642
|
-
|
|
642
|
+
Best-effort block of catastrophic commands (rm -rf of /, a top-level system dir, or
|
|
643
|
+
$HOME/~; fork bombs; whole-drive wipes) — a guard, NOT a sandbox. Soft-warns on
|
|
644
|
+
--force, --no-verify, reset --hard.
|
|
643
645
|
Native Bash remains the fallback for interactive/TTY commands."""
|
|
644
646
|
svc = _svc(ctx)
|
|
645
647
|
|
|
@@ -167,12 +167,18 @@ atexit.register(_cleanup_runtime)
|
|
|
167
167
|
|
|
168
168
|
|
|
169
169
|
# ─── CORS middleware ──────────────────────────────────────
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
170
|
+
# Localhost-only security: Host-header allowlist + Origin/Referer CSRF guard +
|
|
171
|
+
# scoped CORS (no wildcard). This UI server always binds 127.0.0.1, so only
|
|
172
|
+
# loopback origins are accepted. A loopback bind alone does NOT stop a web page
|
|
173
|
+
# in the user's browser from driving these endpoints — see core/web_security.py.
|
|
174
|
+
from core.web_security import (
|
|
175
|
+
allowed_hostnames as _allowed_hostnames,
|
|
176
|
+
)
|
|
177
|
+
from core.web_security import (
|
|
178
|
+
install_guard as _install_web_guard,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
_install_web_guard(app, lambda: _allowed_hostnames(None))
|
|
176
182
|
|
|
177
183
|
|
|
178
184
|
# ─── Serve the UI ─────────────────────────────────────────
|
|
@@ -283,6 +289,11 @@ def api_projects_open():
|
|
|
283
289
|
path = Path(path_str).resolve()
|
|
284
290
|
if not path.exists():
|
|
285
291
|
return jsonify({"error": f"Path does not exist: {path_str}"}), 404
|
|
292
|
+
# Only ever open directories. Opening a *file* via os.startfile would
|
|
293
|
+
# invoke its default handler (e.g. run an .exe/.bat/.lnk), so refuse
|
|
294
|
+
# anything that is not a folder.
|
|
295
|
+
if not path.is_dir():
|
|
296
|
+
return jsonify({"error": "Only directories can be opened"}), 400
|
|
286
297
|
|
|
287
298
|
if sys.platform == "win32":
|
|
288
299
|
os.startfile(str(path))
|
|
@@ -64,10 +64,10 @@ def _filter_text(text: str, depth: str, svc, finalize) -> str:
|
|
|
64
64
|
raw_tokens = res['raw_tokens']
|
|
65
65
|
savings_pct = round((1 - filtered_tokens / raw_tokens) * 100, 1) if raw_tokens > 0 else 0
|
|
66
66
|
|
|
67
|
-
header = f"[filter:{method}] {raw_tokens}
|
|
67
|
+
header = f"[filter:{method}] {raw_tokens}→{filtered_tokens}tok ({savings_pct}%saved)"
|
|
68
68
|
resp = f"{header}\n{result_text}"
|
|
69
69
|
return finalize("c3_filter", {"depth": depth},
|
|
70
|
-
resp, f"{raw_tokens}
|
|
70
|
+
resp, f"{raw_tokens}→{filtered_tokens}tok",
|
|
71
71
|
response_tokens=filtered_tokens)
|
|
72
72
|
|
|
73
73
|
|
|
@@ -25,13 +25,50 @@ def _coerce_list(val: Any) -> list[str] | None:
|
|
|
25
25
|
except (json.JSONDecodeError, ValueError):
|
|
26
26
|
pass
|
|
27
27
|
if val:
|
|
28
|
+
# Comma-separated symbols ("a,b,c") -> multiple targets. Function/class
|
|
29
|
+
# names never contain commas, and regex anchors (^foo$) have none either.
|
|
30
|
+
if "," in val:
|
|
31
|
+
return [s.strip() for s in val.split(",") if s.strip()]
|
|
28
32
|
return [val]
|
|
29
33
|
return None
|
|
30
34
|
|
|
31
35
|
|
|
36
|
+
def _coerce_lines(val: Any):
|
|
37
|
+
"""Coerce `lines` from MCP's string serialization into an int or list.
|
|
38
|
+
|
|
39
|
+
MCP clients sometimes serialize numbers/lists as strings (the same reason
|
|
40
|
+
`_coerce_list` exists for `symbols`). Without this, a JSON-string such as
|
|
41
|
+
"[22, 193]" or "22" falls through handle_read's range logic and the tool
|
|
42
|
+
silently returns the file *map* instead of the requested source lines.
|
|
43
|
+
"""
|
|
44
|
+
if val is None or isinstance(val, (int, list, tuple)):
|
|
45
|
+
return val
|
|
46
|
+
if isinstance(val, str):
|
|
47
|
+
val = val.strip()
|
|
48
|
+
if not val:
|
|
49
|
+
return None
|
|
50
|
+
if val.startswith("["):
|
|
51
|
+
try:
|
|
52
|
+
parsed = json.loads(val)
|
|
53
|
+
except (json.JSONDecodeError, ValueError):
|
|
54
|
+
return None
|
|
55
|
+
return parsed if isinstance(parsed, list) else None
|
|
56
|
+
try:
|
|
57
|
+
return int(val)
|
|
58
|
+
except ValueError:
|
|
59
|
+
if "-" in val: # "start-end" like "22-40"
|
|
60
|
+
a, _, b = val.partition("-")
|
|
61
|
+
try:
|
|
62
|
+
return [int(a.strip()), int(b.strip())]
|
|
63
|
+
except ValueError:
|
|
64
|
+
return None
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
32
68
|
def handle_read(file_path: str, symbols: Any = None, lines: Any = None,
|
|
33
69
|
include_docstrings: bool = True, svc=None, finalize=None) -> str:
|
|
34
70
|
symbols = _coerce_list(symbols)
|
|
71
|
+
lines = _coerce_lines(lines)
|
|
35
72
|
# Multi-file dispatch (parallel)
|
|
36
73
|
if "," in file_path:
|
|
37
74
|
paths = [p.strip() for p in file_path.split(",") if p.strip()]
|
|
@@ -22,9 +22,29 @@ from core import count_tokens
|
|
|
22
22
|
_GIT_MUTATING = re.compile(
|
|
23
23
|
r"^\s*git\s+(commit|add|mv|rm|merge|rebase|cherry-pick|revert|reset|restore|checkout)\b"
|
|
24
24
|
)
|
|
25
|
-
# Hard deny —
|
|
25
|
+
# Hard deny — the handful of genuinely catastrophic, irreversible commands.
|
|
26
|
+
# This is a BEST-EFFORT guard, NOT a sandbox: c3_shell runs arbitrary commands
|
|
27
|
+
# by design and a determined caller can trivially reword around these patterns.
|
|
28
|
+
# The escape hatch for an intentional dangerous command is native Bash.
|
|
29
|
+
# Covered: rm -rf of the filesystem root / a top-level system dir / $HOME / ~,
|
|
30
|
+
# the classic fork bomb, and Windows whole-drive-root wipes (del/rd/format C:\).
|
|
31
|
+
# A top-level system dir only matches when it is the *whole* target, so deleting
|
|
32
|
+
# a nested path like /home/me/project/build is intentionally NOT blocked.
|
|
26
33
|
_BLOCKED = re.compile(
|
|
27
|
-
r"
|
|
34
|
+
r"""
|
|
35
|
+
(?<!git\ )\brm\b (?:\s+-\S+)* \s+ # rm + any flags, then a target:
|
|
36
|
+
(?:
|
|
37
|
+
/(?=\s|$|\*) # filesystem root: / /*
|
|
38
|
+
| ~(?=/|\s|$) # home dir: ~ ~/
|
|
39
|
+
| \$HOME\b # $HOME
|
|
40
|
+
| /(?:etc|usr|bin|sbin|lib|lib64|var|boot|root|home|srv|sys|proc|dev|opt)
|
|
41
|
+
(?=/?(?:\s|$|\*)) # a whole top-level system dir
|
|
42
|
+
)
|
|
43
|
+
| :\(\)\s*\{\s*:\s*\|\s*:\s*\}? # fork bomb :(){ :|: };:
|
|
44
|
+
| \b(?:format|rd|rmdir|del)\b [^\n]*? # windows whole-drive-root wipe
|
|
45
|
+
\b[a-zA-Z]:\\?(?=\s|\*|$|["'])
|
|
46
|
+
""",
|
|
47
|
+
re.IGNORECASE | re.VERBOSE,
|
|
28
48
|
)
|
|
29
49
|
# Soft warn — run but prepend a caveat to the response.
|
|
30
50
|
# `(?<!\w)` / `(?!\w)` anchor against word chars, so `--force` (which starts
|
{code_context_control-2.32.1 → code_context_control-2.33.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.33.0
|
|
4
4
|
Summary: Local code-intelligence layer for AI coding tools (Claude Code, Codex, Gemini, Copilot). Retrieve less, read less, edit safer.
|
|
5
5
|
Author-email: Dimitri Tselenchuk <dtselenc@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -281,6 +281,34 @@ to the C3 edit ledger so the audit trail covers platform-side changes too.
|
|
|
281
281
|
The **Hub UI** (per-project) gains a "Bitbucket" tab with sub-views for
|
|
282
282
|
Overview / Pull Requests / Branches / Activity / Admin.
|
|
283
283
|
|
|
284
|
+
### Oracle Discovery API (v2.32.0)
|
|
285
|
+
|
|
286
|
+
The **Oracle** is C3's optional cross-project memory agent (a local web app). As of
|
|
287
|
+
v2.32.0 it can expose C3's cross-project code & memory intelligence as **tools for an
|
|
288
|
+
external LLM** — point Claude (or any function-calling model) at a running Oracle and
|
|
289
|
+
it can discover your projects and search code, memory, and the cross-project graph
|
|
290
|
+
across all of them.
|
|
291
|
+
|
|
292
|
+
Two transports share one tool core:
|
|
293
|
+
|
|
294
|
+
- **MCP** (streamable HTTP/SSE) at `http://127.0.0.1:3332/mcp` — native for Claude
|
|
295
|
+
Code / Claude Desktop / any MCP client.
|
|
296
|
+
- **OpenAPI REST** at `http://127.0.0.1:3331/api/discovery` — for any LLM with
|
|
297
|
+
function-calling (fetch `/openapi.json` to auto-register the tools).
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
# Start the Oracle (serves the REST + MCP discovery endpoints)
|
|
301
|
+
python oracle/oracle_server.py --no-browser
|
|
302
|
+
|
|
303
|
+
# Print the Bearer token + a ready-to-paste .mcp.json snippet
|
|
304
|
+
c3 oracle api info
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Only **read** and **safe-action** tools are exposed (no code editing); requests need a
|
|
308
|
+
**Bearer token** (stored in the OS keyring) and both servers bind `127.0.0.1` by
|
|
309
|
+
default. Generate, rotate, and copy the token from the dashboard's **Settings →
|
|
310
|
+
Discovery API** tab. See the [Oracle Discovery API guide](oracle-guide/discovery-api.md).
|
|
311
|
+
|
|
284
312
|
---
|
|
285
313
|
|
|
286
314
|
## Tiered local AI (optional)
|
|
@@ -335,7 +363,7 @@ Real-world A/B tests: same task, with and without C3 mounted. Reports include to
|
|
|
335
363
|
|
|
336
364
|
## Security & privacy
|
|
337
365
|
|
|
338
|
-
- **Hub
|
|
366
|
+
- **All web servers (Hub, per-project UI, Oracle) bind to `127.0.0.1` by default and are guarded against browser-based attacks even on loopback** — a Host-header allowlist (defeats DNS rebinding) plus an Origin/Referer check on every request (defeats cross-origin CSRF), with scoped, non-wildcard CORS. A malicious web page you visit therefore cannot drive C3's local endpoints. There is still **no user authentication**, so do not expose these servers to an untrusted network without auth/TLS in front. Binding to a non-loopback interface in `~/.c3/hub_config.json` (`host`) or Oracle's config (`bind_host`) is opt-in and warned at startup; add externally-facing hostnames/IPs to an `allowed_hosts` list there so the guard permits them.
|
|
339
367
|
- **No telemetry by default.** The OSS package collects nothing. Opt-in Sentry crash reporting requires the `[telemetry]` extra plus both `SENTRY_DSN` and `C3_TELEMETRY_OPT_IN=1`. Even when enabled, request bodies, local variables, and prompts are stripped before sending.
|
|
340
368
|
- **API keys** for third-party model providers are read from environment variables and never persisted by C3.
|
|
341
369
|
- See [`SECURITY.md`](SECURITY.md) for the full hardening guide and disclosure policy.
|
|
@@ -355,6 +383,7 @@ The author may introduce a paid offering or relicense future major versions; no
|
|
|
355
383
|
|
|
356
384
|
- **PyPI:** https://pypi.org/project/code-context-control/
|
|
357
385
|
- **Changelog:** [`CHANGELOG.md`](CHANGELOG.md)
|
|
386
|
+
- **Oracle Discovery API:** [`oracle-guide/discovery-api.md`](oracle-guide/discovery-api.md)
|
|
358
387
|
- **Security policy:** [`SECURITY.md`](SECURITY.md)
|
|
359
388
|
- **Licensing FAQ:** [`LICENSING.md`](LICENSING.md)
|
|
360
389
|
- **Issues:** https://github.com/drknowhow/code-context-control/issues
|
|
@@ -69,6 +69,7 @@ code_context_control.egg-info/top_level.txt
|
|
|
69
69
|
core/__init__.py
|
|
70
70
|
core/config.py
|
|
71
71
|
core/ide.py
|
|
72
|
+
core/web_security.py
|
|
72
73
|
oracle/__init__.py
|
|
73
74
|
oracle/config.py
|
|
74
75
|
oracle/mcp_oracle.py
|
|
@@ -168,11 +169,13 @@ tests/test_permissions.py
|
|
|
168
169
|
tests/test_project_manager.py
|
|
169
170
|
tests/test_project_manager_merge.py
|
|
170
171
|
tests/test_project_tool.py
|
|
172
|
+
tests/test_read_coercion.py
|
|
171
173
|
tests/test_session_benchmark.py
|
|
172
174
|
tests/test_session_budget.py
|
|
173
175
|
tests/test_swe_bench.py
|
|
174
176
|
tests/test_tool_registry.py
|
|
175
177
|
tests/test_validate.py
|
|
178
|
+
tests/test_web_security.py
|
|
176
179
|
tests/test_windows_reliability.py
|
|
177
180
|
tui/__init__.py
|
|
178
181
|
tui/backend.py
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Localhost-only security guard for C3's Flask dashboards (UI, Hub, Oracle).
|
|
2
|
+
|
|
3
|
+
Why this exists
|
|
4
|
+
---------------
|
|
5
|
+
C3's web servers bind to loopback (``127.0.0.1``) by default. A loopback bind
|
|
6
|
+
keeps the server off the LAN, but it does **not** protect against requests made
|
|
7
|
+
by a web page running in the user's own browser. Two classic attacks defeat a
|
|
8
|
+
"loopback is safe" assumption:
|
|
9
|
+
|
|
10
|
+
* **Cross-origin / CSRF** — any website the user visits can ``fetch()`` against
|
|
11
|
+
``http://localhost:<port>/...``. With no auth and a permissive CORS policy,
|
|
12
|
+
that page could drive state-changing endpoints (launch an IDE command, add a
|
|
13
|
+
malicious MCP server, downgrade permissions, wipe data).
|
|
14
|
+
* **DNS rebinding** — an attacker domain re-resolves to ``127.0.0.1`` after the
|
|
15
|
+
page loads, so the browser sends requests to the local server with the
|
|
16
|
+
*attacker's* hostname in the ``Host`` header.
|
|
17
|
+
|
|
18
|
+
This module adds two cheap, standard defenses that together close that gap
|
|
19
|
+
without requiring the dashboard JavaScript to change (same-origin requests pass
|
|
20
|
+
naturally):
|
|
21
|
+
|
|
22
|
+
1. **Host-header allowlist** — defeats DNS rebinding. The rebound request
|
|
23
|
+
carries the attacker's hostname in ``Host``; anything not in the allowlist is
|
|
24
|
+
rejected.
|
|
25
|
+
2. **Origin/Referer check on state-changing requests** — defeats cross-origin
|
|
26
|
+
CSRF. Browsers always attach ``Origin`` to ``POST``/``PUT``/``DELETE``/
|
|
27
|
+
``PATCH`` (cross-origin *and* same-origin), so a mismatched origin is a
|
|
28
|
+
reliable CSRF signal.
|
|
29
|
+
|
|
30
|
+
Non-browser clients (curl, the Oracle Discovery REST/MCP consumers) send no
|
|
31
|
+
``Origin`` and a loopback ``Host``, so they are unaffected. Bearer-token auth
|
|
32
|
+
(Oracle discovery) still applies on top of this guard.
|
|
33
|
+
|
|
34
|
+
For an intentional non-loopback bind (an explicit, already-warned opt-in), pass
|
|
35
|
+
the configured bind host so the user can still reach their own dashboard; remote
|
|
36
|
+
hosts that need access can be added via the optional ``extra`` argument
|
|
37
|
+
(wired from an ``allowed_hosts`` config list by the caller).
|
|
38
|
+
"""
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
from collections.abc import Callable, Iterable
|
|
42
|
+
from urllib.parse import urlsplit
|
|
43
|
+
|
|
44
|
+
# Hostnames that always denote "this machine". Note: a literal ``0.0.0.0`` never
|
|
45
|
+
# appears as a browser Host header, so it is intentionally excluded — binding to
|
|
46
|
+
# 0.0.0.0 means the client connects by some concrete IP/name, which must be added
|
|
47
|
+
# via ``extra`` (``allowed_hosts`` config) by the operator.
|
|
48
|
+
_LOOPBACK_HOSTS = frozenset({"localhost", "127.0.0.1", "::1", "[::1]"})
|
|
49
|
+
_MUTATING_METHODS = frozenset({"POST", "PUT", "DELETE", "PATCH"})
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _hostname(value: str | None) -> str:
|
|
53
|
+
"""Extract a bare, lowercased hostname from a Host header or Origin/Referer URL.
|
|
54
|
+
|
|
55
|
+
Handles values with or without a scheme and with or without a port, including
|
|
56
|
+
IPv6 literals like ``[::1]:3333``.
|
|
57
|
+
"""
|
|
58
|
+
if not value:
|
|
59
|
+
return ""
|
|
60
|
+
value = value.strip()
|
|
61
|
+
# A bare Host header ("localhost:3333") has no scheme; prefix "//" so urlsplit
|
|
62
|
+
# parses it as a netloc rather than a path.
|
|
63
|
+
if "://" not in value:
|
|
64
|
+
value = "//" + value
|
|
65
|
+
try:
|
|
66
|
+
host = urlsplit(value).hostname or ""
|
|
67
|
+
except ValueError:
|
|
68
|
+
return ""
|
|
69
|
+
return host.lower()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def allowed_hostnames(bind_host: str | None = None,
|
|
73
|
+
extra: Iterable[str] | None = None) -> set[str]:
|
|
74
|
+
"""Build the set of acceptable hostnames for this server.
|
|
75
|
+
|
|
76
|
+
Always includes loopback names. ``bind_host`` (unless it is the wildcard
|
|
77
|
+
``0.0.0.0`` or empty) and any ``extra`` hosts are added so an intentional
|
|
78
|
+
non-loopback deployment remains reachable.
|
|
79
|
+
"""
|
|
80
|
+
hosts = set(_LOOPBACK_HOSTS)
|
|
81
|
+
bh = (bind_host or "").strip().lower()
|
|
82
|
+
if bh and bh not in ("0.0.0.0", "::", "*"):
|
|
83
|
+
hosts.add(bh)
|
|
84
|
+
if extra:
|
|
85
|
+
for h in extra:
|
|
86
|
+
h = (h or "").strip().lower()
|
|
87
|
+
if h:
|
|
88
|
+
hosts.add(h)
|
|
89
|
+
return hosts
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def check_request(request, allowed: set[str]) -> tuple[bool, str]:
|
|
93
|
+
"""Return ``(ok, reason)``. ``ok == False`` means the request must be 403'd.
|
|
94
|
+
|
|
95
|
+
``request`` is a Flask request (anything exposing ``.host``, ``.method`` and
|
|
96
|
+
``.headers.get``).
|
|
97
|
+
"""
|
|
98
|
+
# 1) Host-header allowlist — anti DNS-rebinding.
|
|
99
|
+
host = _hostname(getattr(request, "host", "") or "")
|
|
100
|
+
if host and host not in allowed:
|
|
101
|
+
return False, f"host '{host}' is not allowlisted"
|
|
102
|
+
|
|
103
|
+
# 2) Origin check — anti cross-origin CSRF. Browsers always send Origin on
|
|
104
|
+
# state-changing requests, so a mismatch is a reliable CSRF signal. When
|
|
105
|
+
# Origin is absent (typical for curl / API clients), fall back to Referer
|
|
106
|
+
# only for mutating methods; a fully header-less request is treated as a
|
|
107
|
+
# non-browser caller and allowed (it cannot be a CSRF from a page).
|
|
108
|
+
origin = request.headers.get("Origin")
|
|
109
|
+
if origin:
|
|
110
|
+
if _hostname(origin) not in allowed:
|
|
111
|
+
return False, "cross-origin request blocked (Origin)"
|
|
112
|
+
elif request.method in _MUTATING_METHODS:
|
|
113
|
+
referer = request.headers.get("Referer")
|
|
114
|
+
if referer and _hostname(referer) not in allowed:
|
|
115
|
+
return False, "cross-origin request blocked (Referer)"
|
|
116
|
+
return True, ""
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def cors_origin(request, allowed: set[str]) -> str | None:
|
|
120
|
+
"""Echo the request Origin in Access-Control-Allow-Origin only if it is
|
|
121
|
+
same-origin/allowlisted. The wildcard ``*`` is never used.
|
|
122
|
+
"""
|
|
123
|
+
origin = request.headers.get("Origin")
|
|
124
|
+
if origin and _hostname(origin) in allowed:
|
|
125
|
+
return origin
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def install_guard(app, get_allowed: Callable[[], set[str]]) -> None:
|
|
130
|
+
"""Register the Host/Origin guard and a tightened CORS policy on a Flask app.
|
|
131
|
+
|
|
132
|
+
``get_allowed`` is called per-request so live config changes (e.g. a hub
|
|
133
|
+
``host`` edit or an ``allowed_hosts`` list) are honoured without a restart.
|
|
134
|
+
Registering this BEFORE any other ``before_request`` (e.g. a bearer-token
|
|
135
|
+
guard) ensures cross-origin requests are rejected first.
|
|
136
|
+
"""
|
|
137
|
+
from flask import jsonify, request
|
|
138
|
+
|
|
139
|
+
@app.before_request
|
|
140
|
+
def _c3_security_guard(): # noqa: ANN202 - Flask hook
|
|
141
|
+
# Let CORS preflight through; the after_request handler answers it and
|
|
142
|
+
# only reflects an allowlisted Origin, so disallowed origins still fail.
|
|
143
|
+
if request.method == "OPTIONS":
|
|
144
|
+
return None
|
|
145
|
+
ok, reason = check_request(request, get_allowed())
|
|
146
|
+
if not ok:
|
|
147
|
+
return jsonify({"error": f"blocked: {reason}"}), 403
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
@app.after_request
|
|
151
|
+
def _c3_cors(response): # noqa: ANN202 - Flask hook
|
|
152
|
+
origin = cors_origin(request, get_allowed())
|
|
153
|
+
if origin:
|
|
154
|
+
response.headers["Access-Control-Allow-Origin"] = origin
|
|
155
|
+
response.headers["Vary"] = "Origin"
|
|
156
|
+
response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization"
|
|
157
|
+
response.headers["Access-Control-Allow-Methods"] = "GET,POST,PUT,DELETE,OPTIONS"
|
|
158
|
+
return response
|
|
@@ -23,12 +23,19 @@ from oracle.services import api_auth
|
|
|
23
23
|
logger = logging.getLogger("oracle.mcp")
|
|
24
24
|
|
|
25
25
|
_INSTRUCTIONS = (
|
|
26
|
-
"C3 Oracle Discovery — cross-project code & memory intelligence as tools
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
26
|
+
"C3 Oracle Discovery — use C3's cross-project code & memory intelligence as tools.\n"
|
|
27
|
+
"\n"
|
|
28
|
+
"Recommended workflow:\n"
|
|
29
|
+
"1. list_projects — see which C3 projects exist (names + absolute paths).\n"
|
|
30
|
+
"2. Discover across ALL projects: search_facts (memory) or c3_search_cross (code).\n"
|
|
31
|
+
"3. Narrow to one project using its path: c3_search to find code; c3_compress "
|
|
32
|
+
"(mode='map') to see a file's shape before reading; c3_read for exact content; "
|
|
33
|
+
"query_memory / read_graph / cross_insights for that project's memory.\n"
|
|
34
|
+
"\n"
|
|
35
|
+
"Notes: per-project tools REQUIRE a `project_path` taken from list_projects. Every tool "
|
|
36
|
+
"returns JSON. suggest_action creates a PENDING suggestion for a human to approve (not a "
|
|
37
|
+
"direct write); delegate_task runs a configured Oracle agent. Read + safe-action tiers "
|
|
38
|
+
"only — no code-editing tools are exposed."
|
|
32
39
|
)
|
|
33
40
|
|
|
34
41
|
|