code-context-control 2.65.0__tar.gz → 2.66.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.65.0/code_context_control.egg-info → code_context_control-2.66.0}/PKG-INFO +1 -1
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/_hook_utils.py +106 -3
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/c3.py +271 -9
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/commands/parser.py +39 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hook_access_guard.py +26 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hook_pretool_enforce.py +167 -45
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_server.py +143 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/app.js +3 -1
- code_context_control-2.66.0/cli/hub_ui/components/hub_enforcement.js +342 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/topbar.js +1 -1
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/server.py +97 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/app.js +4 -0
- code_context_control-2.66.0/cli/ui/components/enforcement.js +334 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/icons.js +1 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0/code_context_control.egg-info}/PKG-INFO +1 -1
- {code_context_control-2.65.0 → code_context_control-2.66.0}/code_context_control.egg-info/SOURCES.txt +7 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/pyproject.toml +1 -1
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/access_guard.py +25 -0
- code_context_control-2.66.0/services/access_telemetry.py +214 -0
- code_context_control-2.66.0/services/enforcement_policy.py +309 -0
- code_context_control-2.66.0/tests/test_enforcement_policy.py +444 -0
- code_context_control-2.66.0/tests/test_enforcement_routes.py +163 -0
- code_context_control-2.66.0/tests/test_hub_enforcement_routes.py +234 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/LICENSE +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/README.md +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/__init__.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/commands/__init__.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/commands/common.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/docs.html +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/edits.html +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/guide/access.html +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/guide/bitbucket.html +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/guide/credentials.html +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/guide/getting-started.html +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/guide/index.html +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/guide/masking.html +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/guide/oracle.html +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/guide/shared.css +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/guide/tools.html +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/guide/workflow.html +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hook_artifact.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hook_auto_snapshot.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hook_c3_signal.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hook_c3read.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hook_dispatch.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hook_edit_ledger.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hook_edit_unlock.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hook_filter.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hook_ghost_files.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hook_prompt_recall.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hook_read.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hook_session_stats.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hook_terse_advisor.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/add_project.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/config_editor.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/drill_artifacts.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/drill_health.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/drill_panel.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/drill_subprojects.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/drill_tasks.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/drill_views.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/global_search.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/hub_credentials.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/hub_locks.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/mcp_manager.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/modals.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/project_card.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/project_tree.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/session_drawer.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/settings_modal.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/sidebar.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/summary_bar.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/task_board.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/components/toasts.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui/state.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/hub_ui.html +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/mcp_proxy.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/mcp_server.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/progress.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/__init__.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/_helpers.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/agent.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/artifacts.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/bitbucket.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/compress.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/credentials.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/delegate.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/edit.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/edits.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/federate.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/filter.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/impact.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/jira.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/locks.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/memory.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/project.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/read.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/search.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/session.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/shell.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/status.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/tasks.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/tools/validate.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/api.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/components/access.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/components/bitbucket.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/components/chat.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/components/credentials.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/components/dashboard.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/components/edits.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/components/instructions.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/components/jira.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/components/memory.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/components/sessions.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/components/settings.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/components/sidebar.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/components/tasks.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/pm_shared.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/shared.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui/theme.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui.html +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui_legacy.html +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/cli/ui_nano.html +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/code_context_control.egg-info/dependency_links.txt +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/code_context_control.egg-info/entry_points.txt +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/code_context_control.egg-info/requires.txt +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/code_context_control.egg-info/top_level.txt +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/core/__init__.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/core/config.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/core/ide.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/core/mcp_toml.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/core/web_security.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/__init__.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/config.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/mcp_oracle.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/oracle_server.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/oracle_ui.html +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/services/__init__.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/services/activity_reporter.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/services/api_auth.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/services/c3_bridge.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/services/chat_engine.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/services/chat_store.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/services/cross_memory.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/services/discovery_audit.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/services/federated_graph.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/services/health_checker.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/services/insight_engine.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/services/local_session.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/services/memory_reader.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/services/memory_writer.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/services/project_scanner.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/services/review_agent.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/services/tool_executor.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/services/tool_registry.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/ui/activity.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/ui/agents.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/ui/app.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/ui/busy.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/ui/chat/conversations.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/ui/chat/input.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/ui/chat/markdown.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/ui/chat/send.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/ui/chat/stream_renderer.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/ui/chat/toolbar.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/ui/core.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/ui/crossgraph.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/ui/header.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/ui/insights.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/ui/projects.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/ui/settings.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/ui/suggestions.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/oracle/ui/theme_tabs.js +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/__init__.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/activity_log.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/agent_base.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/agent_locks.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/agents.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/artifact_defs.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/artifact_store.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/auto_memory.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/bench/__init__.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/bench/external/__init__.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/bench/external/aider_polyglot.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/bench/external/swe_bench.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/benchmark_dashboard.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/bitbucket_client.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/bitbucket_credentials.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/circuit_breaker.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/claude_md.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/compressor.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/context_snapshot.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/conversation_store.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/credential_store.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/doc_index.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/e2e_benchmark.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/e2e_evaluator.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/e2e_tasks.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/edit_ledger.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/embedding_index.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/error_reporting.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/file_memory.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/git_context.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/hub_service.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/indexer.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/jira_client.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/jira_cloud.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/jira_credentials.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/jira_data_center.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/jira_links.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/mask_activation.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/mask_mirror.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/mask_presets.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/memory.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/memory_consolidator.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/memory_distiller.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/memory_graph.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/memory_grounder.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/memory_queue.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/memory_scorer.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/metrics.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/notifications.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/ollama_bridge.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/ollama_client.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/ollama_credentials.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/output_filter.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/parser.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/project_manager.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/project_runtime.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/protocol.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/proxy_state.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/repo_map.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/retention.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/retrieval_broker.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/router.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/runtime.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/scanner.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/session_benchmark.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/session_manager.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/session_preloader.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/subprojects.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/task_store.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/telemetry.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/text_index.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/time_tracker.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/tool_classifier.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/transcript_index.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/validation_cache.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/vector_store.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/watcher.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/services/win_subprocess.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/setup.cfg +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_access_builtin_optout.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_access_guard.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_access_guard_meta.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_access_guard_shell_project.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_access_guard_surfaces.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_access_guard_wiring.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_activity_reporter.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_agent_locks.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_aider_polyglot.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_antigravity_transition.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_artifact_store.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_artifact_tool.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_bitbucket_cli_smoke.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_bitbucket_client.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_bitbucket_config_fallback.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_bitbucket_credentials.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_bitbucket_tool.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_c3_bridge_project_artifacts.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_c3_shell.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_circuit_breaker.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_claude_md_merge.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_cli_smoke.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_compressor_large_file.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_credential_store.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_credentials_routes.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_credentials_tool.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_creds_cli_smoke.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_delegate_cascade.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_e2e_benchmark.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_edit_ledger_hook.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_edit_locking.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_edit_normalization.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_enforcement_flip.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_federated_graph.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_filter_backoff.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_ghost_files.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_ghost_generation.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_git_branch_awareness.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_hook_access_guard.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_hook_dispatch.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_hook_pretool_enforce.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_hook_prompt_recall.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_hook_smoke.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_hook_state.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_hook_utf8_stdio.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_hub_credentials_route.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_hub_credentials_routes_write.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_hub_inspect_api.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_hub_locks_routes.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_hub_server_smoke.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_hub_subproject_links.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_ide_workflow_adaptation.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_init_scan.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_install_mcp_entrypoint.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_jira_cli_smoke.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_jira_client.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_jira_config_fallback.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_jira_credentials.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_jira_links.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_jira_routes.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_jira_tool.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_lazy_store_init.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_mask_guard.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_mask_routes.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_mask_surfaces.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_mcp_host_guard.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_mcp_memory_timeout.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_mcp_server_smoke.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_mcp_toml.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_memory_distiller.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_memory_graph_api.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_memory_queue.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_memory_system.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_no_stale_model_pins.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_notification_dedup.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_notification_discipline.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_ollama_credentials.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_oracle_api_auth.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_oracle_apikey_api.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_oracle_c3_bridge.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_oracle_chat_engine.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_oracle_chat_store.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_oracle_discovery_api.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_oracle_discovery_audit.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_oracle_local_auth.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_oracle_ollama_bridge.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_oracle_scanner_cache.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_oracle_security_fixes.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_oracle_session_bootstrap.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_oracle_subproject_awareness.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_oracle_ui_bundle.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_output_filter.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_permissions.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_pm_api.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_project_manager.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_project_manager_merge.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_project_tool.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_read_coercion.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_read_edit_parity.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_related_facts.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_repo_map.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_response_boilerplate.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_retention.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_review_digest.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_service_durability.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_session_benchmark.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_session_budget.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_shell_creds.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_shell_robustness.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_subproject_exclusion.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_subproject_federation.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_subprojects.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_swe_bench.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_task_store.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_task_tool.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_time_tracker.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_token_telemetry.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_tool_registry.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_upgrade_and_version.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_validate.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_vault_write_guard.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_version_sync.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_web_security.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tests/test_windows_reliability.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tui/__init__.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tui/backend.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tui/main.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tui/screens/__init__.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tui/screens/benchmark_view.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tui/screens/claudemd_view.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tui/screens/compress_view.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tui/screens/index_view.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tui/screens/init_view.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tui/screens/mcp_view.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tui/screens/optimize_view.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tui/screens/pipe_view.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tui/screens/projects_view.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tui/screens/search_view.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tui/screens/session_view.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tui/screens/stats.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tui/screens/ui_view.py +0 -0
- {code_context_control-2.65.0 → code_context_control-2.66.0}/tui/theme.tcss +0 -0
{code_context_control-2.65.0/code_context_control.egg-info → code_context_control-2.66.0}/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.66.0
|
|
4
4
|
Summary: Local MCP code-intelligence for AI coding tools: surgical search/read/edit, agent-config version history, path-level access + masking guards, and a multi-project hub.
|
|
5
5
|
Author-email: Dimitri Tselenchuk <dtselenc@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -96,12 +96,111 @@ def _empty_state(session_id: str = "") -> dict:
|
|
|
96
96
|
return {"session_id": session_id or "", "last_c3_call": None, "unlocked_files": {}}
|
|
97
97
|
|
|
98
98
|
|
|
99
|
+
#: os.replace retries for when Windows briefly holds a handle on the target —
|
|
100
|
+
#: AV scanner, Search indexer, or a concurrent hook reading the same state.
|
|
101
|
+
_REPLACE_ATTEMPTS = 4
|
|
102
|
+
_REPLACE_BACKOFF_S = 0.02
|
|
103
|
+
|
|
104
|
+
|
|
99
105
|
def _atomic_write_json(path: Path, data: dict) -> None:
|
|
100
|
-
"""Write JSON
|
|
106
|
+
"""Write JSON durably: temp file in the same directory + os.replace.
|
|
107
|
+
|
|
108
|
+
Three failure modes this guards against, all observed in the wild
|
|
109
|
+
(10 orphaned .tmp<pid> files and a truncated enforcement_state.json on a
|
|
110
|
+
Windows box before v2.66):
|
|
111
|
+
|
|
112
|
+
1. **Torn write.** Without fsync, os.replace can publish a file whose
|
|
113
|
+
bytes are still in the OS cache. A crash or power loss then leaves a
|
|
114
|
+
zero-length or half-written JSON that every later read quarantines —
|
|
115
|
+
which silently drops all sticky unlocks and makes enforcement *more*
|
|
116
|
+
aggressive, not less.
|
|
117
|
+
2. **Orphaned temp files.** If os.replace raised, the temp file was
|
|
118
|
+
simply abandoned. They accumulate forever in .c3/ and are easy to
|
|
119
|
+
mistake for real state.
|
|
120
|
+
3. **Transient Windows sharing violations.** Another process holding a
|
|
121
|
+
read handle on the target makes os.replace raise PermissionError; a
|
|
122
|
+
single attempt turned that into a lost write.
|
|
123
|
+
"""
|
|
124
|
+
import time
|
|
125
|
+
|
|
101
126
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
102
127
|
tmp = path.with_name(f"{path.name}.tmp{os.getpid()}")
|
|
103
|
-
|
|
104
|
-
|
|
128
|
+
try:
|
|
129
|
+
with open(tmp, "w", encoding="utf-8") as fh:
|
|
130
|
+
json.dump(data, fh)
|
|
131
|
+
fh.flush()
|
|
132
|
+
os.fsync(fh.fileno()) # durability before publish (fixes #1)
|
|
133
|
+
|
|
134
|
+
last_exc = None
|
|
135
|
+
for attempt in range(_REPLACE_ATTEMPTS):
|
|
136
|
+
try:
|
|
137
|
+
os.replace(tmp, path)
|
|
138
|
+
return
|
|
139
|
+
except PermissionError as exc: # fixes #3
|
|
140
|
+
last_exc = exc
|
|
141
|
+
if attempt < _REPLACE_ATTEMPTS - 1:
|
|
142
|
+
time.sleep(_REPLACE_BACKOFF_S * (2 ** attempt))
|
|
143
|
+
if last_exc is not None:
|
|
144
|
+
raise last_exc
|
|
145
|
+
finally:
|
|
146
|
+
# fixes #2 — a successful replace already consumed tmp, so this only
|
|
147
|
+
# fires on the failure paths.
|
|
148
|
+
try:
|
|
149
|
+
if tmp.exists():
|
|
150
|
+
tmp.unlink()
|
|
151
|
+
except OSError:
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def sweep_stale_temps(project_path: Path | None = None) -> int:
|
|
156
|
+
"""Remove orphaned *.tmp<pid> files left by older C3 builds.
|
|
157
|
+
|
|
158
|
+
Only touches temp files whose owning PID is gone, so a concurrent hook
|
|
159
|
+
mid-write is never disturbed. Returns how many were removed; never raises.
|
|
160
|
+
"""
|
|
161
|
+
removed = 0
|
|
162
|
+
try:
|
|
163
|
+
base = project_path if project_path is not None else Path.cwd()
|
|
164
|
+
c3_dir = base / ".c3"
|
|
165
|
+
if not c3_dir.is_dir():
|
|
166
|
+
return 0
|
|
167
|
+
for tmp in c3_dir.glob("*.tmp*"):
|
|
168
|
+
suffix = tmp.name.rsplit(".tmp", 1)[-1]
|
|
169
|
+
if not suffix.isdigit():
|
|
170
|
+
continue
|
|
171
|
+
if _pid_alive(int(suffix)):
|
|
172
|
+
continue
|
|
173
|
+
try:
|
|
174
|
+
tmp.unlink()
|
|
175
|
+
removed += 1
|
|
176
|
+
except OSError:
|
|
177
|
+
pass
|
|
178
|
+
except Exception:
|
|
179
|
+
pass
|
|
180
|
+
return removed
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _pid_alive(pid: int) -> bool:
|
|
184
|
+
"""Best-effort liveness check. On error assume ALIVE (don't delete)."""
|
|
185
|
+
if pid <= 0:
|
|
186
|
+
return False
|
|
187
|
+
try:
|
|
188
|
+
if os.name == "nt":
|
|
189
|
+
import ctypes
|
|
190
|
+
# PROCESS_QUERY_LIMITED_INFORMATION — works without elevation.
|
|
191
|
+
handle = ctypes.windll.kernel32.OpenProcess(0x1000, False, pid)
|
|
192
|
+
if not handle:
|
|
193
|
+
return False
|
|
194
|
+
ctypes.windll.kernel32.CloseHandle(handle)
|
|
195
|
+
return True
|
|
196
|
+
os.kill(pid, 0)
|
|
197
|
+
return True
|
|
198
|
+
except ProcessLookupError:
|
|
199
|
+
return False
|
|
200
|
+
except PermissionError:
|
|
201
|
+
return True # exists but not ours
|
|
202
|
+
except Exception:
|
|
203
|
+
return True # unknown → conservative
|
|
105
204
|
|
|
106
205
|
|
|
107
206
|
def _read_legacy_state(base: Path) -> dict:
|
|
@@ -163,6 +262,10 @@ def load_enforcement_state(project_path: Path | None = None, session_id: str = "
|
|
|
163
262
|
state_path.replace(state_path.with_name(state_path.name + ".corrupt"))
|
|
164
263
|
except Exception:
|
|
165
264
|
pass
|
|
265
|
+
# Corruption is the one reliable signal that a previous write went
|
|
266
|
+
# wrong, so it is also the moment to clear any temp files that
|
|
267
|
+
# write orphaned (pre-v2.66 _atomic_write_json had no cleanup).
|
|
268
|
+
sweep_stale_temps(base)
|
|
166
269
|
STATE_WARNINGS.append(
|
|
167
270
|
"[c3:hook-error] enforcement_state: corrupted "
|
|
168
271
|
f"{ENFORCEMENT_STATE_FILE} quarantined ({type(exc).__name__}); "
|
|
@@ -92,7 +92,7 @@ console = Console() if HAS_RICH else None
|
|
|
92
92
|
# Config
|
|
93
93
|
CONFIG_DIR = ".c3"
|
|
94
94
|
CONFIG_FILE = ".c3/config.json"
|
|
95
|
-
__version__ = "2.
|
|
95
|
+
__version__ = "2.66.0"
|
|
96
96
|
|
|
97
97
|
|
|
98
98
|
def _compress_file_cli(compressor, path, mode="smart", **kw):
|
|
@@ -509,8 +509,15 @@ def _clean_stale_tools(settings_path) -> int:
|
|
|
509
509
|
|
|
510
510
|
|
|
511
511
|
def _apply_permission_tier(project_path: str, tier: str,
|
|
512
|
-
include_mcp_wildcard: bool = False
|
|
513
|
-
|
|
512
|
+
include_mcp_wildcard: bool = False,
|
|
513
|
+
enforcement: str | None = None) -> None:
|
|
514
|
+
"""Write permission tier to .claude/settings.local.json, preserving existing keys.
|
|
515
|
+
|
|
516
|
+
Also derives the matching tool-discipline mode (v2.66+), so a tier means
|
|
517
|
+
what it says: `permissive` no longer leaves every native Edit hard-denied
|
|
518
|
+
by the PreToolUse hook. An explicit `c3 enforce` choice is never
|
|
519
|
+
overwritten here — enforcement_policy.set_mode defers to set_by='user'.
|
|
520
|
+
"""
|
|
514
521
|
tier = _TIER_ALIASES.get(tier, tier)
|
|
515
522
|
settings_path = Path(project_path) / ".claude" / "settings.local.json"
|
|
516
523
|
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -536,6 +543,29 @@ def _apply_permission_tier(project_path: str, tier: str,
|
|
|
536
543
|
suffix = " (+ mcp__* wildcard)" if include_mcp_wildcard else ""
|
|
537
544
|
print(f" Permissions: {tier}{suffix} — {PERMISSION_TIERS[tier]}")
|
|
538
545
|
|
|
546
|
+
_apply_enforcement_for_tier(project_path, tier, enforcement)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def _apply_enforcement_for_tier(project_path: str, tier: str,
|
|
550
|
+
explicit: str | None = None) -> None:
|
|
551
|
+
"""Persist the tool-discipline mode implied by a tier (or an explicit one)."""
|
|
552
|
+
try:
|
|
553
|
+
from services import enforcement_policy as ep
|
|
554
|
+
except Exception:
|
|
555
|
+
return
|
|
556
|
+
mode = explicit or ep.derive_from_tier(tier)
|
|
557
|
+
set_by = ep.SET_BY_USER if explicit else ep.SET_BY_TIER
|
|
558
|
+
try:
|
|
559
|
+
result = ep.set_mode(mode, project_path, set_by=set_by, scope="project")
|
|
560
|
+
except ValueError as exc:
|
|
561
|
+
print(f" [warn] Could not set tool discipline: {exc}")
|
|
562
|
+
return
|
|
563
|
+
if result.get("deferred"):
|
|
564
|
+
print(f" Discipline : {result['mode']} (kept — set explicitly via "
|
|
565
|
+
f"`c3 enforce`; tier would have used '{mode}')")
|
|
566
|
+
return
|
|
567
|
+
print(f" Discipline : {result['mode']} — {ep.MODE_HELP[result['mode']]}")
|
|
568
|
+
|
|
539
569
|
|
|
540
570
|
def _check_c3_health(project_path: str) -> dict:
|
|
541
571
|
"""Inspect an existing .c3 installation and return a health report."""
|
|
@@ -821,7 +851,7 @@ def _prompt_init_steps(project_path: str, ide_name: str, default_mode: str = "di
|
|
|
821
851
|
|
|
822
852
|
print()
|
|
823
853
|
git_choice = _prompt_choice(
|
|
824
|
-
"Step 2/
|
|
854
|
+
"Step 2/5 — Initialize a local Git repository for this project?",
|
|
825
855
|
[
|
|
826
856
|
"Yes — run local git init in this folder",
|
|
827
857
|
"No — leave version control untouched",
|
|
@@ -836,7 +866,7 @@ def _prompt_init_steps(project_path: str, ide_name: str, default_mode: str = "di
|
|
|
836
866
|
|
|
837
867
|
print()
|
|
838
868
|
install_choice = _prompt_choice(
|
|
839
|
-
"Step 3/
|
|
869
|
+
"Step 3/5 — Install MCP tooling for this project?",
|
|
840
870
|
[
|
|
841
871
|
"Yes — configure the IDE and wire up C3 MCP",
|
|
842
872
|
"No — skip MCP install for now",
|
|
@@ -855,13 +885,13 @@ def _prompt_init_steps(project_path: str, ide_name: str, default_mode: str = "di
|
|
|
855
885
|
)
|
|
856
886
|
mcp_mode = "proxy" if mode_choice and mode_choice.startswith("Proxy") else default_mode
|
|
857
887
|
|
|
858
|
-
# Step 4/
|
|
888
|
+
# Step 4/5 — Permissions (Claude Code only)
|
|
859
889
|
chosen_tier = None
|
|
860
890
|
include_wildcard = False
|
|
861
891
|
if chosen_ide == "claude-code":
|
|
862
892
|
print()
|
|
863
893
|
tier_choice = _prompt_choice(
|
|
864
|
-
"Step 4/
|
|
894
|
+
"Step 4/5 — Set a Claude Code permission tier for this project?",
|
|
865
895
|
[
|
|
866
896
|
"standard — full editing + safe shell, block destructive ops (recommended)",
|
|
867
897
|
"c3-strict — c3_* MCP tools only, deny native Read/Grep/Glob/Edit/Write",
|
|
@@ -885,11 +915,45 @@ def _prompt_init_steps(project_path: str, ide_name: str, default_mode: str = "di
|
|
|
885
915
|
)
|
|
886
916
|
include_wildcard = bool(wildcard_choice and wildcard_choice.startswith("Yes"))
|
|
887
917
|
|
|
918
|
+
chosen_enforcement = _prompt_enforcement(chosen_tier)
|
|
919
|
+
|
|
888
920
|
_run_install_mcp(project_path, chosen_ide, mcp_mode=mcp_mode,
|
|
889
|
-
permissions=chosen_tier, include_mcp_wildcard=include_wildcard
|
|
921
|
+
permissions=chosen_tier, include_mcp_wildcard=include_wildcard,
|
|
922
|
+
enforcement=chosen_enforcement)
|
|
890
923
|
return chosen_ide, True
|
|
891
924
|
|
|
892
925
|
|
|
926
|
+
def _prompt_enforcement(chosen_tier: str | None) -> str | None:
|
|
927
|
+
"""Step 5/5 — tool discipline. Returns an explicit mode, or None to derive.
|
|
928
|
+
|
|
929
|
+
Asked separately from the permission tier because it answers a genuinely
|
|
930
|
+
different question: the tier controls what the IDE will let the agent call,
|
|
931
|
+
this controls how hard C3's own hook pushes it toward c3_* tools. Users hit
|
|
932
|
+
friction here, not in the tier, and previously had no way to change it.
|
|
933
|
+
"""
|
|
934
|
+
try:
|
|
935
|
+
from services import enforcement_policy as ep
|
|
936
|
+
except Exception:
|
|
937
|
+
return None
|
|
938
|
+
|
|
939
|
+
default_mode = ep.derive_from_tier(chosen_tier) if chosen_tier else ep.MODE_ADVISORY
|
|
940
|
+
print()
|
|
941
|
+
print(f" (Your permission tier suggests: {default_mode})")
|
|
942
|
+
choice = _prompt_choice(
|
|
943
|
+
"Step 5/5 — Tool discipline: how hard should C3 push toward c3_* tools?",
|
|
944
|
+
[
|
|
945
|
+
"advisory — allow native Edit/Write with a nudge; ledger still logs (recommended)",
|
|
946
|
+
"strict — block native Edit/Write until a c3_* call runs first",
|
|
947
|
+
"off — no nudging (Access Guard + vault guard still enforce)",
|
|
948
|
+
f"Use tier default ({default_mode})",
|
|
949
|
+
],
|
|
950
|
+
)
|
|
951
|
+
if not choice or choice.startswith("Use tier default"):
|
|
952
|
+
return None
|
|
953
|
+
mode = choice.split()[0]
|
|
954
|
+
return mode if mode in ep.MODES else None
|
|
955
|
+
|
|
956
|
+
|
|
893
957
|
def _parse_cli_ide_arg(value: str) -> str:
|
|
894
958
|
"""Parse public CLI IDE names while still accepting legacy aliases."""
|
|
895
959
|
raw = (value or "").strip().lower()
|
|
@@ -1033,6 +1097,7 @@ def cmd_init(args):
|
|
|
1033
1097
|
mcp_mode=getattr(args, "mcp_mode", "direct"),
|
|
1034
1098
|
permissions=getattr(args, "permissions", None),
|
|
1035
1099
|
include_mcp_wildcard=bool(getattr(args, "include_mcp_wildcard", False)),
|
|
1100
|
+
enforcement=getattr(args, "enforcement", None),
|
|
1036
1101
|
)
|
|
1037
1102
|
else:
|
|
1038
1103
|
_prompt_init_steps(project_path, requested_ide, default_mode=getattr(args, "mcp_mode", "direct"))
|
|
@@ -1112,6 +1177,35 @@ def cmd_init(args):
|
|
|
1112
1177
|
except Exception:
|
|
1113
1178
|
pass
|
|
1114
1179
|
|
|
1180
|
+
# Tool discipline (Layer C) — the knob users actually feel day to day.
|
|
1181
|
+
try:
|
|
1182
|
+
from services import enforcement_policy as _ep
|
|
1183
|
+
_pol = _ep.resolve(project_path)
|
|
1184
|
+
_bits = [_pol.mode]
|
|
1185
|
+
if _pol.scope == "default":
|
|
1186
|
+
_bits.append("default — never set")
|
|
1187
|
+
elif _pol.set_by:
|
|
1188
|
+
_bits.append(f"set by {_pol.set_by}")
|
|
1189
|
+
_stored_tier = _safe_read_json(
|
|
1190
|
+
c3_dir / "config.json", "config").get("permission_tier")
|
|
1191
|
+
if _stored_tier and _ep.derive_from_tier(_stored_tier) != _pol.mode:
|
|
1192
|
+
_bits.append(f"tier '{_stored_tier}' implies "
|
|
1193
|
+
f"'{_ep.derive_from_tier(_stored_tier)}'")
|
|
1194
|
+
print(f" Disc : {', '.join(_bits)} (change: c3 enforce <mode>)")
|
|
1195
|
+
for _w in _pol.warnings:
|
|
1196
|
+
print(f" [warn] {_w}")
|
|
1197
|
+
except Exception:
|
|
1198
|
+
pass
|
|
1199
|
+
|
|
1200
|
+
# Orphaned hook temp files (pre-v2.66 atomic-write bug).
|
|
1201
|
+
try:
|
|
1202
|
+
from cli._hook_utils import sweep_stale_temps as _sweep
|
|
1203
|
+
_swept = _sweep(Path(project_path))
|
|
1204
|
+
if _swept:
|
|
1205
|
+
print(f" Fixed : removed {_swept} orphaned .c3 temp file(s)")
|
|
1206
|
+
except Exception:
|
|
1207
|
+
pass
|
|
1208
|
+
|
|
1115
1209
|
# Delegate backends
|
|
1116
1210
|
try:
|
|
1117
1211
|
from services.ollama_client import OllamaClient as _OC
|
|
@@ -4935,6 +5029,7 @@ def _cmd_permissions_clean(settings_path: Path) -> None:
|
|
|
4935
5029
|
def _run_install_mcp(project_path: str, ide_name: str, mcp_mode: str = "direct",
|
|
4936
5030
|
permissions: str | None = None,
|
|
4937
5031
|
include_mcp_wildcard: bool = False,
|
|
5032
|
+
enforcement: str | None = None,
|
|
4938
5033
|
banner: str = "Installing MCP tools...") -> None:
|
|
4939
5034
|
"""Run install-mcp programmatically with a consistent banner."""
|
|
4940
5035
|
print(f"\n{banner}")
|
|
@@ -4942,6 +5037,7 @@ def _run_install_mcp(project_path: str, ide_name: str, mcp_mode: str = "direct",
|
|
|
4942
5037
|
cmd_install_mcp(SimpleNamespace(
|
|
4943
5038
|
project_path=project_path, ide=ide_name, mcp_mode=mcp_mode,
|
|
4944
5039
|
permissions=permissions, include_mcp_wildcard=include_mcp_wildcard,
|
|
5040
|
+
enforcement=enforcement,
|
|
4945
5041
|
))
|
|
4946
5042
|
|
|
4947
5043
|
|
|
@@ -5346,6 +5442,14 @@ def cmd_install_mcp(args):
|
|
|
5346
5442
|
with open(c3_config_path, "w", encoding="utf-8") as f:
|
|
5347
5443
|
json.dump(_c3cfg, f, indent=2)
|
|
5348
5444
|
|
|
5445
|
+
# Tool discipline (Layer C) rides along with the tier so `permissive`
|
|
5446
|
+
# actually means permissive at the hook layer too. An explicit
|
|
5447
|
+
# --enforcement wins; otherwise the tier derives it.
|
|
5448
|
+
_enf = getattr(args, "enforcement", None)
|
|
5449
|
+
if (perm_tier and perm_tier in PERMISSION_TIERS) or _enf:
|
|
5450
|
+
_apply_enforcement_for_tier(
|
|
5451
|
+
str(target), perm_tier or "standard", _enf)
|
|
5452
|
+
|
|
5349
5453
|
with open(settings_path, 'w', encoding="utf-8") as f:
|
|
5350
5454
|
json.dump(settings, f, indent=2)
|
|
5351
5455
|
|
|
@@ -6002,13 +6106,117 @@ def _locks_audit(target: str, res: dict, project_path: str, note: str) -> None:
|
|
|
6002
6106
|
pass
|
|
6003
6107
|
|
|
6004
6108
|
|
|
6109
|
+
def cmd_enforce(args):
|
|
6110
|
+
"""`c3 enforce [strict|advisory|off]` — the Layer C knob.
|
|
6111
|
+
|
|
6112
|
+
Deliberately a separate command from `c3 access`: that one governs which
|
|
6113
|
+
PATHS the agent may touch (a security boundary), this one governs how hard
|
|
6114
|
+
C3 pushes it toward c3_* tools (a workflow preference). Conflating them is
|
|
6115
|
+
what made "the guard is slowing me down" unfixable without weakening
|
|
6116
|
+
something that should not be weakened.
|
|
6117
|
+
"""
|
|
6118
|
+
from services import enforcement_policy as ep
|
|
6119
|
+
|
|
6120
|
+
project_path = str(Path(getattr(args, "project_path", ".") or ".").resolve())
|
|
6121
|
+
mode = getattr(args, "mode", None)
|
|
6122
|
+
scope = "global" if getattr(args, "use_global", False) else "project"
|
|
6123
|
+
ttl = getattr(args, "signal_ttl", None)
|
|
6124
|
+
|
|
6125
|
+
if not mode and ttl is None:
|
|
6126
|
+
_enforce_show(project_path, ep)
|
|
6127
|
+
return
|
|
6128
|
+
|
|
6129
|
+
current = ep.resolve(project_path)
|
|
6130
|
+
try:
|
|
6131
|
+
result = ep.set_mode(mode or current.mode, project_path,
|
|
6132
|
+
set_by=ep.SET_BY_USER, scope=scope,
|
|
6133
|
+
signal_ttl_s=ttl)
|
|
6134
|
+
except ValueError as exc:
|
|
6135
|
+
print(f"Error: {exc}")
|
|
6136
|
+
return
|
|
6137
|
+
|
|
6138
|
+
print_header("Tool discipline")
|
|
6139
|
+
print(f" Mode : {result['mode']} — {ep.MODE_HELP[result['mode']]}")
|
|
6140
|
+
print(f" Scope : {result['scope']} ({result['path']})")
|
|
6141
|
+
if ttl is not None:
|
|
6142
|
+
print(f" Signal TTL: {ttl}s")
|
|
6143
|
+
if not result["changed"] and mode:
|
|
6144
|
+
print(" (already set to this mode)")
|
|
6145
|
+
|
|
6146
|
+
if result["mode"] == ep.MODE_OFF:
|
|
6147
|
+
print("\n Still enforced regardless of this setting:")
|
|
6148
|
+
print(" - Access Guard path rules (c3 access list)")
|
|
6149
|
+
print(" - Credential vault guard (.c3/secrets.enc, cred_state.json)")
|
|
6150
|
+
print(" - Agent locks (c3 locks list)")
|
|
6151
|
+
if result["mode"] != ep.MODE_STRICT:
|
|
6152
|
+
print("\n The edit ledger still records native writes (PostToolUse).")
|
|
6153
|
+
print(" What you lose vs strict: the pre-edit snapshot c3_edit takes.")
|
|
6154
|
+
|
|
6155
|
+
_enforce_audit(result, project_path)
|
|
6156
|
+
|
|
6157
|
+
|
|
6158
|
+
def _enforce_show(project_path: str, ep) -> None:
|
|
6159
|
+
"""Print the effective policy plus the tier it would be derived from."""
|
|
6160
|
+
policy = ep.resolve(project_path)
|
|
6161
|
+
print_header("Tool discipline")
|
|
6162
|
+
print(f" Mode : {policy.describe()}")
|
|
6163
|
+
print(f" Signal TTL: {policy.signal_ttl_s}s")
|
|
6164
|
+
blocked = ", ".join(sorted(policy.blocked_tools)) or "(none)"
|
|
6165
|
+
print(f" Blocks: {blocked}" if policy.blocks_writes
|
|
6166
|
+
else f" Blocks: nothing (would be {blocked} under strict)")
|
|
6167
|
+
|
|
6168
|
+
cfg = _safe_read_json(Path(project_path) / ".c3" / "config.json", "config")
|
|
6169
|
+
tier = cfg.get("permission_tier")
|
|
6170
|
+
if tier:
|
|
6171
|
+
derived = ep.derive_from_tier(tier)
|
|
6172
|
+
note = "" if derived == policy.mode else " <- differs from active mode"
|
|
6173
|
+
print(f" Tier : {tier} would derive '{derived}'{note}")
|
|
6174
|
+
|
|
6175
|
+
for warn in policy.warnings:
|
|
6176
|
+
print(f" [warn] {warn}")
|
|
6177
|
+
|
|
6178
|
+
print("\n Modes:")
|
|
6179
|
+
for name in ep.MODES:
|
|
6180
|
+
marker = "*" if name == policy.mode else " "
|
|
6181
|
+
print(f" {marker} {name:9s} {ep.MODE_HELP[name]}")
|
|
6182
|
+
print("\n Change: c3 enforce advisory | see cost: c3 access stats")
|
|
6183
|
+
|
|
6184
|
+
|
|
6185
|
+
def _enforce_audit(result: dict, project_path: str) -> None:
|
|
6186
|
+
"""Ledger + activity entry — enforcement changes are security-adjacent."""
|
|
6187
|
+
try:
|
|
6188
|
+
from services.activity_log import ActivityLog
|
|
6189
|
+
ActivityLog(project_path).log("access_action", {
|
|
6190
|
+
"kind": "enforcement", "action": "set_mode",
|
|
6191
|
+
"mode": result["mode"], "previous": result.get("previous", ""),
|
|
6192
|
+
"scope": result["scope"], "set_by": result["set_by"],
|
|
6193
|
+
})
|
|
6194
|
+
except Exception:
|
|
6195
|
+
pass
|
|
6196
|
+
try:
|
|
6197
|
+
from services.edit_ledger import EditLedger
|
|
6198
|
+
EditLedger(project_path).log_edit(
|
|
6199
|
+
file=f"enforcement://{result['scope']}",
|
|
6200
|
+
change_type="enforcement_set_mode",
|
|
6201
|
+
summary=(f"tool discipline {result.get('previous') or 'default'} "
|
|
6202
|
+
f"-> {result['mode']} ({result['scope']}) via `c3 enforce`"),
|
|
6203
|
+
tags=["enforcement", "access"],
|
|
6204
|
+
detail={"kind": "enforcement", "mode": result["mode"],
|
|
6205
|
+
"previous": result.get("previous", ""),
|
|
6206
|
+
"scope": result["scope"], "set_by": result["set_by"]},
|
|
6207
|
+
)
|
|
6208
|
+
except Exception:
|
|
6209
|
+
pass
|
|
6210
|
+
|
|
6211
|
+
|
|
6005
6212
|
def cmd_access(args):
|
|
6006
6213
|
"""Access Guard rule management — human-only mutation surface (spec §1)."""
|
|
6007
6214
|
sub = getattr(args, "access_cmd", None)
|
|
6008
6215
|
if not sub:
|
|
6009
|
-
print("Usage: c3 access {list,add,remove,check,mask,builtin} [args]")
|
|
6216
|
+
print("Usage: c3 access {list,add,remove,check,stats,mask,builtin} [args]")
|
|
6010
6217
|
print(" c3 access mask {add,rm,status,activate,preview} [args]")
|
|
6011
6218
|
print(" c3 access builtin {disable,enable} <glob>")
|
|
6219
|
+
print(" c3 access stats [--session ID] [--clear] [--json]")
|
|
6012
6220
|
return
|
|
6013
6221
|
|
|
6014
6222
|
project_path = getattr(args, "project_path", ".") or "."
|
|
@@ -6021,6 +6229,8 @@ def cmd_access(args):
|
|
|
6021
6229
|
_access_cmd_remove(args, project_path)
|
|
6022
6230
|
elif sub == "check":
|
|
6023
6231
|
_access_cmd_check(args, project_path)
|
|
6232
|
+
elif sub == "stats":
|
|
6233
|
+
_access_cmd_stats(args, project_path)
|
|
6024
6234
|
elif sub == "mask":
|
|
6025
6235
|
_access_cmd_mask(args, project_path)
|
|
6026
6236
|
elif sub == "builtin":
|
|
@@ -6029,6 +6239,57 @@ def cmd_access(args):
|
|
|
6029
6239
|
print(f"Unknown access subcommand: {sub}")
|
|
6030
6240
|
|
|
6031
6241
|
|
|
6242
|
+
def _access_cmd_stats(args, project_path: str) -> None:
|
|
6243
|
+
"""`c3 access stats` — turn felt friction into a ranked, actionable list."""
|
|
6244
|
+
from services import access_telemetry as at
|
|
6245
|
+
|
|
6246
|
+
if getattr(args, "clear", False):
|
|
6247
|
+
removed = at.clear(project_path)
|
|
6248
|
+
print(f"[OK] Cleared denial log ({removed} file(s)).")
|
|
6249
|
+
return
|
|
6250
|
+
|
|
6251
|
+
agg = at.aggregate(project_path, session_id=getattr(args, "session", "") or "")
|
|
6252
|
+
|
|
6253
|
+
if getattr(args, "as_json", False):
|
|
6254
|
+
print(json.dumps(agg, indent=2))
|
|
6255
|
+
return
|
|
6256
|
+
|
|
6257
|
+
print_header("Denials")
|
|
6258
|
+
if not agg["total"]:
|
|
6259
|
+
print(" Nothing recorded. Either nothing was denied, or this C3 "
|
|
6260
|
+
"predates denial\n telemetry (v2.66.0) — it only logs from the "
|
|
6261
|
+
"first denial after upgrade.")
|
|
6262
|
+
return
|
|
6263
|
+
|
|
6264
|
+
by_layer = agg["by_layer"]
|
|
6265
|
+
print(f" {agg['total']} denial(s) across {agg['sessions']} session(s)")
|
|
6266
|
+
print(f" path policy (Access Guard) : {by_layer.get('access', 0)}")
|
|
6267
|
+
print(f" tool discipline (c3 enforce) : {by_layer.get('discipline', 0)}")
|
|
6268
|
+
print()
|
|
6269
|
+
|
|
6270
|
+
limit = max(1, int(getattr(args, "limit", 15) or 15))
|
|
6271
|
+
rows = agg["rows"][:limit]
|
|
6272
|
+
width = max((len(r["rule"]) for r in rows), default=8)
|
|
6273
|
+
print(f" {'HITS':>5} {'RULE'.ljust(width)} {'TOOL':<12} LAYER")
|
|
6274
|
+
for row in rows:
|
|
6275
|
+
print(f" {row['hits']:>5} {row['rule'].ljust(width)} "
|
|
6276
|
+
f"{row['tool']:<12} {row['layer']}")
|
|
6277
|
+
|
|
6278
|
+
print("\n How to unblock the top offenders:")
|
|
6279
|
+
seen: set = set()
|
|
6280
|
+
for row in rows[:5]:
|
|
6281
|
+
fix = at.suggest(row)
|
|
6282
|
+
if fix in seen:
|
|
6283
|
+
continue
|
|
6284
|
+
seen.add(fix)
|
|
6285
|
+
print(f" - {row['rule']}: {fix}")
|
|
6286
|
+
|
|
6287
|
+
remaining = len(agg["rows"]) - len(rows)
|
|
6288
|
+
if remaining > 0:
|
|
6289
|
+
print(f"\n ... {remaining} more (use --limit)")
|
|
6290
|
+
print("\n Reset counters: c3 access stats --clear")
|
|
6291
|
+
|
|
6292
|
+
|
|
6032
6293
|
def _access_scope(args) -> str:
|
|
6033
6294
|
return "global" if getattr(args, "use_global", False) else "project"
|
|
6034
6295
|
|
|
@@ -7732,6 +7993,7 @@ def main():
|
|
|
7732
7993
|
"jira": cmd_jira,
|
|
7733
7994
|
"creds": cmd_creds,
|
|
7734
7995
|
"access": cmd_access,
|
|
7996
|
+
"enforce": cmd_enforce,
|
|
7735
7997
|
"locks": cmd_locks,
|
|
7736
7998
|
"oracle": cmd_oracle,
|
|
7737
7999
|
"upgrade": cmd_upgrade,
|
|
@@ -26,6 +26,7 @@ def build_parser(version: str, parse_cli_ide_arg):
|
|
|
26
26
|
p_init.add_argument("--git", action="store_true", help="Initialize a local Git repository during init/update")
|
|
27
27
|
p_init.add_argument("--no-embed", action="store_true", help="Skip building the semantic embedding index during init")
|
|
28
28
|
p_init.add_argument("--permissions", choices=["read-only", "c3-strict", "standard", "permissive"], default=None, help="Apply Claude Code permission tier (Claude Code only, used with --force)")
|
|
29
|
+
p_init.add_argument("--enforcement", choices=["strict", "advisory", "off"], default=None, help="Tool-discipline mode. Omit to derive from --permissions (standard->advisory, permissive->off, strict/read-only->strict)")
|
|
29
30
|
p_init.add_argument("--include-mcp-wildcard", action="store_true", help="Add mcp__* wildcard so non-C3 MCP servers don't prompt per-call")
|
|
30
31
|
|
|
31
32
|
p_upgrade = subparsers.add_parser("upgrade", help="Upgrade C3 to the latest PyPI release")
|
|
@@ -471,6 +472,28 @@ def build_parser(version: str, parse_cli_ide_arg):
|
|
|
471
472
|
lk_sweep = locks_subs.add_parser("sweep", help="Drop expired leases now")
|
|
472
473
|
lk_sweep.add_argument("--path", dest="project_path", default=".")
|
|
473
474
|
|
|
475
|
+
# ── Tool discipline / enforcement mode (v2.66.0) ────────────────────
|
|
476
|
+
# LAYER C: how hard C3 pushes the agent toward c3_* tools. Distinct from
|
|
477
|
+
# `c3 access` (path policy — a security boundary) and from
|
|
478
|
+
# `c3 permissions` (the IDE's own allow/deny lists).
|
|
479
|
+
p_enforce = subparsers.add_parser(
|
|
480
|
+
"enforce",
|
|
481
|
+
help="Tool discipline — strict | advisory | off (show current if omitted)",
|
|
482
|
+
)
|
|
483
|
+
p_enforce.add_argument(
|
|
484
|
+
"mode", nargs="?", default=None,
|
|
485
|
+
choices=["strict", "advisory", "off"],
|
|
486
|
+
help="strict = block native Edit/Write until a c3_* call; "
|
|
487
|
+
"advisory = allow with a nudge (ledger still logs); "
|
|
488
|
+
"off = no nudging. Omit to show the current mode.",
|
|
489
|
+
)
|
|
490
|
+
p_enforce.add_argument("--global", dest="use_global", action="store_true",
|
|
491
|
+
help="Write to the global scope (~/.c3) as the default for every project")
|
|
492
|
+
p_enforce.add_argument("--signal-ttl", type=int, default=None, dest="signal_ttl",
|
|
493
|
+
help="Seconds a c3_* call keeps native tools unlocked (default 600)")
|
|
494
|
+
p_enforce.add_argument("--path", dest="project_path", default=".",
|
|
495
|
+
help="Project directory (default: current)")
|
|
496
|
+
|
|
474
497
|
# ── Access Guard (v2.62.0) ──────────────────────────────────────────
|
|
475
498
|
# Human-only mutation surface (frozen spec docs/access-guard.md §1):
|
|
476
499
|
# rule changes happen here or in the UI tab, never via an agent tool.
|
|
@@ -507,6 +530,22 @@ def build_parser(version: str, parse_cli_ide_arg):
|
|
|
507
530
|
help="Operation to evaluate (default: read)")
|
|
508
531
|
ac_check.add_argument("--path", dest="project_path", default=".", help="Project directory (default: current)")
|
|
509
532
|
|
|
533
|
+
# ── Denial telemetry (v2.66.0, docs/access-guard.md §3) ─────────────
|
|
534
|
+
# Answers "which rule is actually costing me time" with hit counts, and
|
|
535
|
+
# names the lever that would clear each one.
|
|
536
|
+
ac_stats = access_subs.add_parser(
|
|
537
|
+
"stats", help="What got denied, how often, and how to unblock it")
|
|
538
|
+
ac_stats.add_argument("--path", dest="project_path", default=".",
|
|
539
|
+
help="Project directory (default: current)")
|
|
540
|
+
ac_stats.add_argument("--session", default="",
|
|
541
|
+
help="Limit to one session id (default: all retained)")
|
|
542
|
+
ac_stats.add_argument("--limit", type=int, default=15,
|
|
543
|
+
help="Max rows to show (default: 15)")
|
|
544
|
+
ac_stats.add_argument("--clear", action="store_true",
|
|
545
|
+
help="Delete the retained denial log and exit")
|
|
546
|
+
ac_stats.add_argument("--json", dest="as_json", action="store_true",
|
|
547
|
+
help="Emit the aggregate as JSON")
|
|
548
|
+
|
|
510
549
|
# ── Builtin opt-out (two-key) ───────────────────────────────────────
|
|
511
550
|
# Builtins are on by default. Switching one off needs a config entry AND
|
|
512
551
|
# a keyring attestation, so an agent that writes config.json alone still
|
|
@@ -23,6 +23,11 @@ from _hook_utils import normalize_tool_name # noqa: E402
|
|
|
23
23
|
|
|
24
24
|
from services import access_guard as ag # noqa: E402
|
|
25
25
|
|
|
26
|
+
try:
|
|
27
|
+
from services import access_telemetry
|
|
28
|
+
except Exception: # pragma: no cover — telemetry is never load-bearing
|
|
29
|
+
access_telemetry = None
|
|
30
|
+
|
|
26
31
|
_WRITE_TOOLS = {"Edit", "Write", "MultiEdit", "NotebookEdit"}
|
|
27
32
|
_READ_TOOLS = {"Read", "SearchText"}
|
|
28
33
|
_SEARCH_TOOLS = {"Grep", "Glob", "FindFiles"}
|
|
@@ -59,6 +64,23 @@ def _deny(reason: str) -> dict:
|
|
|
59
64
|
}
|
|
60
65
|
|
|
61
66
|
|
|
67
|
+
def _record(denial, tool: str, operation: str, path: str,
|
|
68
|
+
base: str, session_id: str = "") -> None:
|
|
69
|
+
"""Log a path-policy denial for `c3 access stats`. Best-effort."""
|
|
70
|
+
if access_telemetry is None or denial is None:
|
|
71
|
+
return
|
|
72
|
+
try:
|
|
73
|
+
access_telemetry.record(
|
|
74
|
+
layer=access_telemetry.LAYER_ACCESS,
|
|
75
|
+
rule=getattr(denial, "rule", ""),
|
|
76
|
+
scope=getattr(denial, "scope", ""),
|
|
77
|
+
tool=tool, operation=operation, path=path,
|
|
78
|
+
session_id=session_id, project_path=base,
|
|
79
|
+
)
|
|
80
|
+
except Exception:
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
|
|
62
84
|
def _target(tool_input: dict) -> str:
|
|
63
85
|
return str(
|
|
64
86
|
tool_input.get("file_path")
|
|
@@ -105,6 +127,7 @@ def run(payload: dict, project_path: Path | None = None) -> dict | None:
|
|
|
105
127
|
tool = normalize_tool_name(payload.get("tool_name", ""))
|
|
106
128
|
tool_input = payload.get("tool_input", {}) or {}
|
|
107
129
|
base = str(project_path if project_path is not None else Path.cwd())
|
|
130
|
+
session_id = str(payload.get("session_id") or "")
|
|
108
131
|
|
|
109
132
|
if tool in _WRITE_TOOLS or tool in _READ_TOOLS:
|
|
110
133
|
fp = _target(tool_input)
|
|
@@ -113,6 +136,7 @@ def run(payload: dict, project_path: Path | None = None) -> dict | None:
|
|
|
113
136
|
op = "write" if tool in _WRITE_TOOLS else "read"
|
|
114
137
|
denial = ag.check(fp, op, base)
|
|
115
138
|
if denial:
|
|
139
|
+
_record(denial, tool, op, fp, base, session_id)
|
|
116
140
|
return _deny(ag.refusal(denial, fp, op, surface="hook", tool=tool))
|
|
117
141
|
return None
|
|
118
142
|
|
|
@@ -123,6 +147,7 @@ def run(payload: dict, project_path: Path | None = None) -> dict | None:
|
|
|
123
147
|
if root:
|
|
124
148
|
denial = ag.check(root, "read", base)
|
|
125
149
|
if denial and denial.kind == "deny":
|
|
150
|
+
_record(denial, tool, "read", root, base, session_id)
|
|
126
151
|
return _deny(ag.refusal(denial, root, "read",
|
|
127
152
|
surface="hook", tool=tool))
|
|
128
153
|
footer = ag.search_footer(base)
|
|
@@ -134,6 +159,7 @@ def run(payload: dict, project_path: Path | None = None) -> dict | None:
|
|
|
134
159
|
return None
|
|
135
160
|
denial, tok = _scan_shell(cmd, base)
|
|
136
161
|
if denial:
|
|
162
|
+
_record(denial, tool, "read", tok, base, session_id)
|
|
137
163
|
return _deny(ag.refusal(denial, tok, "read",
|
|
138
164
|
surface="hook", tool=tool)
|
|
139
165
|
+ " (best-effort shell scan)")
|