code-context-control 2.32.2__tar.gz → 2.34.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {code_context_control-2.32.2 → code_context_control-2.34.0}/PKG-INFO +2 -2
- {code_context_control-2.32.2 → code_context_control-2.34.0}/README.md +1 -1
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/c3.py +21 -4
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/hook_ghost_files.py +12 -2
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/hub_server.py +31 -107
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/mcp_server.py +3 -1
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/server.py +31 -114
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/tools/filter.py +2 -2
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/tools/read.py +37 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/tools/shell.py +81 -5
- {code_context_control-2.32.2 → code_context_control-2.34.0}/code_context_control.egg-info/PKG-INFO +2 -2
- {code_context_control-2.32.2 → code_context_control-2.34.0}/code_context_control.egg-info/SOURCES.txt +7 -0
- code_context_control-2.34.0/core/mcp_toml.py +128 -0
- code_context_control-2.34.0/core/web_security.py +174 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/mcp_oracle.py +56 -11
- {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/oracle_server.py +16 -6
- {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/services/tool_registry.py +17 -2
- {code_context_control-2.32.2 → code_context_control-2.34.0}/pyproject.toml +1 -1
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/claude_md.py +1 -1
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_c3_shell.py +34 -0
- 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_read_coercion.py +68 -0
- code_context_control-2.34.0/tests/test_shell_robustness.py +90 -0
- code_context_control-2.34.0/tests/test_web_security.py +106 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/LICENSE +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/__init__.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/_hook_utils.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/commands/__init__.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/commands/common.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/commands/parser.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/docs.html +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/edits.html +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/hook_auto_snapshot.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/hook_c3_signal.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/hook_c3read.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/hook_edit_ledger.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/hook_edit_unlock.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/hook_filter.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/hook_pretool_enforce.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/hook_read.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/hook_session_stats.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/hook_terse_advisor.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/hub.html +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/mcp_proxy.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/tools/__init__.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/tools/_helpers.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/tools/agent.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/tools/bitbucket.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/tools/compress.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/tools/delegate.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/tools/edit.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/tools/edits.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/tools/impact.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/tools/memory.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/tools/project.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/tools/search.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/tools/session.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/tools/status.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/tools/validate.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/ui/api.js +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/ui/app.js +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/ui/components/bitbucket.js +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/ui/components/chat.js +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/ui/components/dashboard.js +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/ui/components/edits.js +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/ui/components/instructions.js +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/ui/components/memory.js +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/ui/components/sessions.js +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/ui/components/settings.js +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/ui/components/sidebar.js +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/ui/icons.js +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/ui/shared.js +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/ui/theme.js +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/ui.html +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/ui_legacy.html +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/cli/ui_nano.html +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/code_context_control.egg-info/dependency_links.txt +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/code_context_control.egg-info/entry_points.txt +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/code_context_control.egg-info/requires.txt +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/code_context_control.egg-info/top_level.txt +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/core/__init__.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/core/config.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/core/ide.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/__init__.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/config.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/oracle.html +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/services/__init__.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/services/api_auth.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/services/c3_bridge.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/services/chat_engine.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/services/chat_store.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/services/cross_memory.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/services/federated_graph.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/services/health_checker.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/services/insight_engine.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/services/memory_reader.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/services/memory_writer.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/services/ollama_bridge.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/services/project_scanner.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/services/review_agent.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/oracle/services/tool_executor.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/__init__.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/activity_log.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/agent_base.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/agents.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/auto_memory.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/bench/__init__.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/bench/external/__init__.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/bench/external/aider_polyglot.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/bench/external/swe_bench.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/benchmark_dashboard.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/bitbucket_client.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/bitbucket_credentials.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/compressor.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/context_snapshot.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/conversation_store.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/doc_index.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/e2e_benchmark.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/e2e_evaluator.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/e2e_tasks.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/edit_ledger.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/embedding_index.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/error_reporting.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/file_memory.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/hub_service.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/indexer.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/memory.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/memory_consolidator.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/memory_graph.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/memory_grounder.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/memory_scorer.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/metrics.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/notifications.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/ollama_client.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/output_filter.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/parser.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/project_manager.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/project_runtime.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/protocol.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/proxy_state.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/retrieval_broker.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/router.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/runtime.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/session_benchmark.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/session_manager.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/session_preloader.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/text_index.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/tool_classifier.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/transcript_index.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/validation_cache.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/vector_store.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/version_tracker.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/services/watcher.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/setup.cfg +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_aider_polyglot.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_bitbucket_cli_smoke.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_bitbucket_client.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_bitbucket_credentials.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_bitbucket_tool.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_cli_smoke.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_e2e_benchmark.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_edit_normalization.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_enforcement_flip.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_federated_graph.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_ghost_files.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_hub_server_smoke.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_mcp_server_smoke.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_memory_graph_api.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_memory_system.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_notification_discipline.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_oracle_api_auth.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_oracle_apikey_api.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_oracle_discovery_api.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_output_filter.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_permissions.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_project_manager.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_project_manager_merge.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_project_tool.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_session_benchmark.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_session_budget.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_swe_bench.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_tool_registry.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_validate.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tests/test_windows_reliability.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tui/__init__.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tui/backend.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tui/main.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tui/screens/__init__.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tui/screens/benchmark_view.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tui/screens/claudemd_view.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tui/screens/compress_view.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tui/screens/index_view.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tui/screens/init_view.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tui/screens/mcp_view.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tui/screens/optimize_view.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tui/screens/pipe_view.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tui/screens/projects_view.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tui/screens/search_view.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tui/screens/session_view.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tui/screens/stats.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tui/screens/ui_view.py +0 -0
- {code_context_control-2.32.2 → code_context_control-2.34.0}/tui/theme.tcss +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-context-control
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.34.0
|
|
4
4
|
Summary: Local code-intelligence layer for AI coding tools (Claude Code, Codex, Gemini, Copilot). Retrieve less, read less, edit safer.
|
|
5
5
|
Author-email: Dimitri Tselenchuk <dtselenc@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -363,7 +363,7 @@ Real-world A/B tests: same task, with and without C3 mounted. Reports include to
|
|
|
363
363
|
|
|
364
364
|
## Security & privacy
|
|
365
365
|
|
|
366
|
-
- **Hub
|
|
366
|
+
- **All web servers (Hub, per-project UI, Oracle) bind to `127.0.0.1` by default and are guarded against browser-based attacks even on loopback** — a Host-header allowlist (defeats DNS rebinding) plus an Origin/Referer check on every request (defeats cross-origin CSRF), with scoped, non-wildcard CORS. A malicious web page you visit therefore cannot drive C3's local endpoints. There is still **no user authentication**, so do not expose these servers to an untrusted network without auth/TLS in front. Binding to a non-loopback interface in `~/.c3/hub_config.json` (`host`) or Oracle's config (`bind_host`) is opt-in and warned at startup; add externally-facing hostnames/IPs to an `allowed_hosts` list there so the guard permits them. _(Cross-origin/CSRF + DNS-rebinding hardening added in v2.33.0.)_
|
|
367
367
|
- **No telemetry by default.** The OSS package collects nothing. Opt-in Sentry crash reporting requires the `[telemetry]` extra plus both `SENTRY_DSN` and `C3_TELEMETRY_OPT_IN=1`. Even when enabled, request bodies, local variables, and prompts are stripped before sending.
|
|
368
368
|
- **API keys** for third-party model providers are read from environment variables and never persisted by C3.
|
|
369
369
|
- See [`SECURITY.md`](SECURITY.md) for the full hardening guide and disclosure policy.
|
|
@@ -301,7 +301,7 @@ Real-world A/B tests: same task, with and without C3 mounted. Reports include to
|
|
|
301
301
|
|
|
302
302
|
## Security & privacy
|
|
303
303
|
|
|
304
|
-
- **Hub
|
|
304
|
+
- **All web servers (Hub, per-project UI, Oracle) bind to `127.0.0.1` by default and are guarded against browser-based attacks even on loopback** — a Host-header allowlist (defeats DNS rebinding) plus an Origin/Referer check on every request (defeats cross-origin CSRF), with scoped, non-wildcard CORS. A malicious web page you visit therefore cannot drive C3's local endpoints. There is still **no user authentication**, so do not expose these servers to an untrusted network without auth/TLS in front. Binding to a non-loopback interface in `~/.c3/hub_config.json` (`host`) or Oracle's config (`bind_host`) is opt-in and warned at startup; add externally-facing hostnames/IPs to an `allowed_hosts` list there so the guard permits them. _(Cross-origin/CSRF + DNS-rebinding hardening added in v2.33.0.)_
|
|
305
305
|
- **No telemetry by default.** The OSS package collects nothing. Opt-in Sentry crash reporting requires the `[telemetry]` extra plus both `SENTRY_DSN` and `C3_TELEMETRY_OPT_IN=1`. Even when enabled, request bodies, local variables, and prompts are stripped before sending.
|
|
306
306
|
- **API keys** for third-party model providers are read from environment variables and never persisted by C3.
|
|
307
307
|
- See [`SECURITY.md`](SECURITY.md) for the full hardening guide and disclosure policy.
|
|
@@ -85,7 +85,7 @@ console = Console() if HAS_RICH else None
|
|
|
85
85
|
# Config
|
|
86
86
|
CONFIG_DIR = ".c3"
|
|
87
87
|
CONFIG_FILE = ".c3/config.json"
|
|
88
|
-
__version__ = "2.
|
|
88
|
+
__version__ = "2.34.0"
|
|
89
89
|
|
|
90
90
|
|
|
91
91
|
def _command_deps() -> CommandDeps:
|
|
@@ -4921,8 +4921,14 @@ def cmd_install_mcp(args):
|
|
|
4921
4921
|
# Build hook commands using the Python executable that runs c3.
|
|
4922
4922
|
# On Windows, Claude Code executes hooks via /usr/bin/bash (Git Bash), which cannot
|
|
4923
4923
|
# parse Windows absolute paths containing parentheses (e.g. "(C3)"). Prefix with
|
|
4924
|
-
#
|
|
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"
|
|
@@ -160,46 +180,12 @@ def _project_mcp_config_path(project_root: Path, profile) -> Path:
|
|
|
160
180
|
return (Path.home() / profile.config_path) if profile.config_path_global else (project_root / profile.config_path)
|
|
161
181
|
|
|
162
182
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if not line:
|
|
170
|
-
continue
|
|
171
|
-
|
|
172
|
-
if line.startswith("[") and line.endswith("]"):
|
|
173
|
-
section = line[1:-1].strip()
|
|
174
|
-
if section.startswith("mcp_servers."):
|
|
175
|
-
current_server = section.split(".", 1)[1]
|
|
176
|
-
servers.setdefault(current_server, {})
|
|
177
|
-
else:
|
|
178
|
-
current_server = None
|
|
179
|
-
continue
|
|
180
|
-
|
|
181
|
-
if not current_server or "=" not in line:
|
|
182
|
-
continue
|
|
183
|
-
|
|
184
|
-
key, value = line.split("=", 1)
|
|
185
|
-
key = key.strip().strip('"')
|
|
186
|
-
value = value.strip()
|
|
187
|
-
|
|
188
|
-
if key == "args":
|
|
189
|
-
servers[current_server]["args"] = re.findall(r"[\"']([^\"']*)[\"']", value)
|
|
190
|
-
elif key in ("command", "type"):
|
|
191
|
-
match = re.match(r"^[\"'](.*)[\"']$", value)
|
|
192
|
-
servers[current_server][key] = match.group(1) if match else value
|
|
193
|
-
elif key == "enabled":
|
|
194
|
-
low = value.lower()
|
|
195
|
-
if low.startswith("true"):
|
|
196
|
-
servers[current_server]["enabled"] = True
|
|
197
|
-
elif low.startswith("false"):
|
|
198
|
-
servers[current_server]["enabled"] = False
|
|
199
|
-
else:
|
|
200
|
-
servers[current_server][key] = value
|
|
201
|
-
|
|
202
|
-
return servers
|
|
183
|
+
from core.mcp_toml import (
|
|
184
|
+
parse_toml_mcp_servers as _parse_toml_mcp_servers,
|
|
185
|
+
)
|
|
186
|
+
from core.mcp_toml import (
|
|
187
|
+
upsert_toml_section as _upsert_toml_section,
|
|
188
|
+
)
|
|
203
189
|
|
|
204
190
|
|
|
205
191
|
def _read_project_mcp_servers_for_profile(profile, mcp_file: Path) -> tuple[dict, dict]:
|
|
@@ -218,73 +204,6 @@ def _read_project_mcp_servers_for_profile(profile, mcp_file: Path) -> tuple[dict
|
|
|
218
204
|
return servers, raw_config
|
|
219
205
|
|
|
220
206
|
|
|
221
|
-
def _toml_escape_str(value: str) -> str:
|
|
222
|
-
return value.replace("\\", "/")
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
def _upsert_toml_section(toml_path: Path, section: str, entries: dict) -> None:
|
|
226
|
-
content = toml_path.read_text(encoding="utf-8") if toml_path.exists() else ""
|
|
227
|
-
header = f"[{section}]"
|
|
228
|
-
|
|
229
|
-
lines = content.splitlines()
|
|
230
|
-
new_lines = []
|
|
231
|
-
skip = False
|
|
232
|
-
for line in lines:
|
|
233
|
-
stripped = line.strip()
|
|
234
|
-
if stripped == header:
|
|
235
|
-
skip = True
|
|
236
|
-
continue
|
|
237
|
-
if skip and stripped.startswith("["):
|
|
238
|
-
skip = False
|
|
239
|
-
if not skip:
|
|
240
|
-
new_lines.append(line)
|
|
241
|
-
|
|
242
|
-
content = "\n".join(new_lines).rstrip()
|
|
243
|
-
section_lines = [f"\n\n{header}"]
|
|
244
|
-
for key, value in entries.items():
|
|
245
|
-
if isinstance(value, list):
|
|
246
|
-
items = ", ".join(f'"{_toml_escape_str(str(item))}"' for item in value)
|
|
247
|
-
section_lines.append(f'{key} = [{items}]')
|
|
248
|
-
elif isinstance(value, bool):
|
|
249
|
-
section_lines.append(f'{key} = {"true" if value else "false"}')
|
|
250
|
-
else:
|
|
251
|
-
section_lines.append(f'{key} = "{_toml_escape_str(str(value))}"')
|
|
252
|
-
section_lines.append("")
|
|
253
|
-
|
|
254
|
-
toml_path.parent.mkdir(parents=True, exist_ok=True)
|
|
255
|
-
toml_path.write_text(content + "\n".join(section_lines), encoding="utf-8")
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
def _remove_toml_section(toml_path: Path, section: str) -> bool:
|
|
259
|
-
if not toml_path.exists():
|
|
260
|
-
return False
|
|
261
|
-
content = toml_path.read_text(encoding="utf-8")
|
|
262
|
-
header = f"[{section}]"
|
|
263
|
-
|
|
264
|
-
lines = content.splitlines()
|
|
265
|
-
new_lines = []
|
|
266
|
-
skip = False
|
|
267
|
-
removed = False
|
|
268
|
-
for line in lines:
|
|
269
|
-
stripped = line.strip()
|
|
270
|
-
if stripped == header:
|
|
271
|
-
skip = True
|
|
272
|
-
removed = True
|
|
273
|
-
continue
|
|
274
|
-
if skip and stripped.startswith("["):
|
|
275
|
-
skip = False
|
|
276
|
-
if not skip:
|
|
277
|
-
new_lines.append(line)
|
|
278
|
-
|
|
279
|
-
if removed:
|
|
280
|
-
remaining = "\n".join(new_lines).rstrip()
|
|
281
|
-
if remaining:
|
|
282
|
-
toml_path.write_text(remaining + "\n", encoding="utf-8")
|
|
283
|
-
else:
|
|
284
|
-
toml_path.unlink()
|
|
285
|
-
return removed
|
|
286
|
-
|
|
287
|
-
|
|
288
207
|
def _build_mcp_cli_capabilities() -> dict:
|
|
289
208
|
return {
|
|
290
209
|
"commands": [
|
|
@@ -519,6 +438,11 @@ def api_projects_open():
|
|
|
519
438
|
path = Path(path_str).resolve()
|
|
520
439
|
if not path.exists():
|
|
521
440
|
return jsonify({"error": f"Path does not exist: {path_str}"}), 404
|
|
441
|
+
# Only ever open directories. Opening a *file* via os.startfile would
|
|
442
|
+
# invoke its default handler (e.g. run an .exe/.bat/.lnk), so refuse
|
|
443
|
+
# anything that is not a folder.
|
|
444
|
+
if not path.is_dir():
|
|
445
|
+
return jsonify({"error": "Only directories can be opened"}), 400
|
|
522
446
|
|
|
523
447
|
if sys.platform == "win32":
|
|
524
448
|
os.startfile(str(path))
|
|
@@ -639,7 +639,9 @@ async def c3_shell(cmd: str, cwd: str = "", timeout: int = 60,
|
|
|
639
639
|
"""EXECUTE shell command — structured returns, auto-filter, ledger-aware.
|
|
640
640
|
Use for tests, git, build, scripts. Returns exit_code/stdout/stderr/duration_ms.
|
|
641
641
|
Auto-filters stdout >30 lines; auto-logs git mutations to the edit ledger.
|
|
642
|
-
|
|
642
|
+
Best-effort block of catastrophic commands (rm -rf of /, a top-level system dir, or
|
|
643
|
+
$HOME/~; fork bombs; whole-drive wipes) — a guard, NOT a sandbox. Soft-warns on
|
|
644
|
+
--force, --no-verify, reset --hard.
|
|
643
645
|
Native Bash remains the fallback for interactive/TTY commands."""
|
|
644
646
|
svc = _svc(ctx)
|
|
645
647
|
|
|
@@ -10,7 +10,6 @@ import csv
|
|
|
10
10
|
import json
|
|
11
11
|
import logging
|
|
12
12
|
import os
|
|
13
|
-
import re
|
|
14
13
|
import signal
|
|
15
14
|
import subprocess
|
|
16
15
|
import sys
|
|
@@ -167,12 +166,21 @@ atexit.register(_cleanup_runtime)
|
|
|
167
166
|
|
|
168
167
|
|
|
169
168
|
# ─── CORS middleware ──────────────────────────────────────
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
169
|
+
# Localhost-only security: Host-header allowlist + Origin/Referer CSRF guard +
|
|
170
|
+
# scoped CORS (no wildcard). This UI server always binds 127.0.0.1, so only
|
|
171
|
+
# loopback origins are accepted. A loopback bind alone does NOT stop a web page
|
|
172
|
+
# in the user's browser from driving these endpoints — see core/web_security.py.
|
|
173
|
+
from core.web_security import (
|
|
174
|
+
allowed_hostnames as _allowed_hostnames,
|
|
175
|
+
)
|
|
176
|
+
from core.web_security import (
|
|
177
|
+
guard_summary as _guard_summary,
|
|
178
|
+
)
|
|
179
|
+
from core.web_security import (
|
|
180
|
+
install_guard as _install_web_guard,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
_install_web_guard(app, lambda: _allowed_hostnames(None))
|
|
176
184
|
|
|
177
185
|
|
|
178
186
|
# ─── Serve the UI ─────────────────────────────────────────
|
|
@@ -283,6 +291,11 @@ def api_projects_open():
|
|
|
283
291
|
path = Path(path_str).resolve()
|
|
284
292
|
if not path.exists():
|
|
285
293
|
return jsonify({"error": f"Path does not exist: {path_str}"}), 404
|
|
294
|
+
# Only ever open directories. Opening a *file* via os.startfile would
|
|
295
|
+
# invoke its default handler (e.g. run an .exe/.bat/.lnk), so refuse
|
|
296
|
+
# anything that is not a folder.
|
|
297
|
+
if not path.is_dir():
|
|
298
|
+
return jsonify({"error": "Only directories can be opened"}), 400
|
|
286
299
|
|
|
287
300
|
if sys.platform == "win32":
|
|
288
301
|
os.startfile(str(path))
|
|
@@ -365,7 +378,8 @@ def api_health():
|
|
|
365
378
|
except Exception:
|
|
366
379
|
pass
|
|
367
380
|
|
|
368
|
-
return jsonify({"service": "c3-ui", "sources": sources, "session": session_info
|
|
381
|
+
return jsonify({"service": "c3-ui", "sources": sources, "session": session_info,
|
|
382
|
+
"web_guard": _guard_summary()})
|
|
369
383
|
|
|
370
384
|
|
|
371
385
|
# ─── API: Session Registry ───────────────────────────────
|
|
@@ -2410,47 +2424,15 @@ def api_proxy_tools():
|
|
|
2410
2424
|
|
|
2411
2425
|
|
|
2412
2426
|
# ─── API: MCP Status ─────────────────────────────────────
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
if line.startswith("[") and line.endswith("]"):
|
|
2424
|
-
section = line[1:-1].strip()
|
|
2425
|
-
if section.startswith("mcp_servers."):
|
|
2426
|
-
current_server = section.split(".", 1)[1]
|
|
2427
|
-
servers.setdefault(current_server, {})
|
|
2428
|
-
else:
|
|
2429
|
-
current_server = None
|
|
2430
|
-
continue
|
|
2431
|
-
|
|
2432
|
-
if not current_server or "=" not in line:
|
|
2433
|
-
continue
|
|
2434
|
-
|
|
2435
|
-
key, value = line.split("=", 1)
|
|
2436
|
-
key = key.strip()
|
|
2437
|
-
value = value.strip()
|
|
2438
|
-
|
|
2439
|
-
if key == "args":
|
|
2440
|
-
servers[current_server]["args"] = re.findall(r"[\"']([^\"']*)[\"']", value)
|
|
2441
|
-
elif key in ("command", "type"):
|
|
2442
|
-
match = re.match(r"^[\"'](.*)[\"']$", value)
|
|
2443
|
-
servers[current_server][key] = match.group(1) if match else value
|
|
2444
|
-
elif key == "enabled":
|
|
2445
|
-
low = value.lower()
|
|
2446
|
-
if low.startswith("true"):
|
|
2447
|
-
servers[current_server]["enabled"] = True
|
|
2448
|
-
elif low.startswith("false"):
|
|
2449
|
-
servers[current_server]["enabled"] = False
|
|
2450
|
-
else:
|
|
2451
|
-
servers[current_server][key] = value
|
|
2452
|
-
|
|
2453
|
-
return servers
|
|
2427
|
+
from core.mcp_toml import (
|
|
2428
|
+
parse_toml_mcp_servers as _parse_toml_mcp_servers,
|
|
2429
|
+
)
|
|
2430
|
+
from core.mcp_toml import (
|
|
2431
|
+
remove_toml_section as _remove_toml_section,
|
|
2432
|
+
)
|
|
2433
|
+
from core.mcp_toml import (
|
|
2434
|
+
upsert_toml_section as _upsert_toml_section,
|
|
2435
|
+
)
|
|
2454
2436
|
|
|
2455
2437
|
|
|
2456
2438
|
def _find_server_script(servers: dict) -> bool:
|
|
@@ -2463,71 +2445,6 @@ def _find_server_script(servers: dict) -> bool:
|
|
|
2463
2445
|
return False
|
|
2464
2446
|
|
|
2465
2447
|
|
|
2466
|
-
def _toml_escape_str(value: str) -> str:
|
|
2467
|
-
return value.replace("\\", "/")
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
def _upsert_toml_section(toml_path: Path, section: str, entries: dict) -> None:
|
|
2471
|
-
"""Add or replace a dotted TOML section in-place."""
|
|
2472
|
-
content = toml_path.read_text(encoding="utf-8") if toml_path.exists() else ""
|
|
2473
|
-
header = f"[{section}]"
|
|
2474
|
-
|
|
2475
|
-
lines = content.splitlines()
|
|
2476
|
-
new_lines = []
|
|
2477
|
-
skip = False
|
|
2478
|
-
for line in lines:
|
|
2479
|
-
stripped = line.strip()
|
|
2480
|
-
if stripped == header:
|
|
2481
|
-
skip = True
|
|
2482
|
-
continue
|
|
2483
|
-
if skip and stripped.startswith("["):
|
|
2484
|
-
skip = False
|
|
2485
|
-
if not skip:
|
|
2486
|
-
new_lines.append(line)
|
|
2487
|
-
|
|
2488
|
-
content = "\n".join(new_lines).rstrip()
|
|
2489
|
-
section_lines = [f"\n\n{header}"]
|
|
2490
|
-
for k, v in entries.items():
|
|
2491
|
-
if isinstance(v, list):
|
|
2492
|
-
items = ", ".join(f'"{_toml_escape_str(str(x))}"' for x in v)
|
|
2493
|
-
section_lines.append(f'{k} = [{items}]')
|
|
2494
|
-
elif isinstance(v, bool):
|
|
2495
|
-
section_lines.append(f'{k} = {"true" if v else "false"}')
|
|
2496
|
-
else:
|
|
2497
|
-
section_lines.append(f'{k} = "{_toml_escape_str(str(v))}"')
|
|
2498
|
-
section_lines.append("")
|
|
2499
|
-
|
|
2500
|
-
toml_path.parent.mkdir(parents=True, exist_ok=True)
|
|
2501
|
-
toml_path.write_text(content + "\n".join(section_lines), encoding="utf-8")
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
def _remove_toml_section(toml_path: Path, section: str) -> bool:
|
|
2505
|
-
"""Remove a dotted TOML section. Returns True if removed."""
|
|
2506
|
-
if not toml_path.exists():
|
|
2507
|
-
return False
|
|
2508
|
-
content = toml_path.read_text(encoding="utf-8")
|
|
2509
|
-
header = f"[{section}]"
|
|
2510
|
-
|
|
2511
|
-
lines = content.splitlines()
|
|
2512
|
-
new_lines = []
|
|
2513
|
-
skip = False
|
|
2514
|
-
removed = False
|
|
2515
|
-
for line in lines:
|
|
2516
|
-
stripped = line.strip()
|
|
2517
|
-
if stripped == header:
|
|
2518
|
-
skip = True
|
|
2519
|
-
removed = True
|
|
2520
|
-
continue
|
|
2521
|
-
if skip and stripped.startswith("["):
|
|
2522
|
-
skip = False
|
|
2523
|
-
if not skip:
|
|
2524
|
-
new_lines.append(line)
|
|
2525
|
-
|
|
2526
|
-
if removed:
|
|
2527
|
-
toml_path.write_text("\n".join(new_lines).rstrip() + "\n", encoding="utf-8")
|
|
2528
|
-
return removed
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
2448
|
def _resolve_mcp_profile(ide_name: str | None):
|
|
2532
2449
|
requested = (ide_name or "").strip().lower()
|
|
2533
2450
|
if requested and requested != "auto":
|
|
@@ -64,10 +64,10 @@ def _filter_text(text: str, depth: str, svc, finalize) -> str:
|
|
|
64
64
|
raw_tokens = res['raw_tokens']
|
|
65
65
|
savings_pct = round((1 - filtered_tokens / raw_tokens) * 100, 1) if raw_tokens > 0 else 0
|
|
66
66
|
|
|
67
|
-
header = f"[filter:{method}] {raw_tokens}
|
|
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()]
|