code-context-control 2.71.0__tar.gz → 2.74.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (411) hide show
  1. {code_context_control-2.71.0/code_context_control.egg-info → code_context_control-2.74.0}/PKG-INFO +1 -1
  2. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/c3.py +1 -1
  3. code_context_control-2.74.0/cli/tools/_grants.py +61 -0
  4. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/tools/compress.py +4 -1
  5. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/tools/edit.py +10 -12
  6. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/tools/filter.py +4 -1
  7. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/tools/impact.py +3 -1
  8. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/tools/locks.py +4 -7
  9. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/tools/override.py +3 -4
  10. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/tools/read.py +8 -1
  11. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/tools/validate.py +5 -2
  12. {code_context_control-2.71.0 → code_context_control-2.74.0/code_context_control.egg-info}/PKG-INFO +1 -1
  13. {code_context_control-2.71.0 → code_context_control-2.74.0}/code_context_control.egg-info/SOURCES.txt +4 -0
  14. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/services/mobile_api.py +132 -8
  15. {code_context_control-2.71.0 → code_context_control-2.74.0}/pyproject.toml +1 -1
  16. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/access_guard.py +38 -10
  17. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/notifications.py +22 -1
  18. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/override_policy.py +31 -0
  19. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/override_requests.py +30 -0
  20. code_context_control-2.74.0/services/override_wake.py +241 -0
  21. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_mobile_api.py +113 -1
  22. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_mobile_override_routes.py +39 -0
  23. code_context_control-2.74.0/tests/test_override_grants_mcp.py +189 -0
  24. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_override_requests.py +125 -0
  25. code_context_control-2.74.0/tests/test_override_wake.py +278 -0
  26. {code_context_control-2.71.0 → code_context_control-2.74.0}/LICENSE +0 -0
  27. {code_context_control-2.71.0 → code_context_control-2.74.0}/README.md +0 -0
  28. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/__init__.py +0 -0
  29. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/_hook_utils.py +0 -0
  30. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/commands/__init__.py +0 -0
  31. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/commands/common.py +0 -0
  32. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/commands/parser.py +0 -0
  33. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/docs.html +0 -0
  34. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/edits.html +0 -0
  35. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/guide/access.html +0 -0
  36. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/guide/bitbucket.html +0 -0
  37. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/guide/credentials.html +0 -0
  38. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/guide/getting-started.html +0 -0
  39. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/guide/index.html +0 -0
  40. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/guide/masking.html +0 -0
  41. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/guide/oracle.html +0 -0
  42. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/guide/shared.css +0 -0
  43. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/guide/tools.html +0 -0
  44. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/guide/workflow.html +0 -0
  45. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hook_access_guard.py +0 -0
  46. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hook_artifact.py +0 -0
  47. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hook_auto_snapshot.py +0 -0
  48. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hook_c3_signal.py +0 -0
  49. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hook_c3read.py +0 -0
  50. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hook_dispatch.py +0 -0
  51. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hook_edit_ledger.py +0 -0
  52. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hook_edit_unlock.py +0 -0
  53. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hook_filter.py +0 -0
  54. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hook_ghost_files.py +0 -0
  55. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hook_pretool_enforce.py +0 -0
  56. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hook_prompt_recall.py +0 -0
  57. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hook_read.py +0 -0
  58. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hook_session_stats.py +0 -0
  59. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hook_terse_advisor.py +0 -0
  60. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hub_server.py +0 -0
  61. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hub_ui/app.js +0 -0
  62. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hub_ui/components/add_project.js +0 -0
  63. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hub_ui/components/config_editor.js +0 -0
  64. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hub_ui/components/drill_artifacts.js +0 -0
  65. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hub_ui/components/drill_health.js +0 -0
  66. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hub_ui/components/drill_panel.js +0 -0
  67. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hub_ui/components/drill_subprojects.js +0 -0
  68. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hub_ui/components/drill_tasks.js +0 -0
  69. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hub_ui/components/drill_views.js +0 -0
  70. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hub_ui/components/global_search.js +0 -0
  71. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hub_ui/components/hub_credentials.js +0 -0
  72. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hub_ui/components/hub_enforcement.js +0 -0
  73. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hub_ui/components/hub_locks.js +0 -0
  74. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hub_ui/components/mcp_manager.js +0 -0
  75. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hub_ui/components/modals.js +0 -0
  76. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hub_ui/components/project_card.js +0 -0
  77. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hub_ui/components/project_tree.js +0 -0
  78. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hub_ui/components/session_drawer.js +0 -0
  79. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hub_ui/components/settings_modal.js +0 -0
  80. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hub_ui/components/sidebar.js +0 -0
  81. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hub_ui/components/summary_bar.js +0 -0
  82. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hub_ui/components/task_board.js +0 -0
  83. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hub_ui/components/toasts.js +0 -0
  84. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hub_ui/components/topbar.js +0 -0
  85. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hub_ui/state.js +0 -0
  86. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/hub_ui.html +0 -0
  87. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/mcp_proxy.py +0 -0
  88. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/mcp_server.py +0 -0
  89. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/progress.py +0 -0
  90. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/server.py +0 -0
  91. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/tools/__init__.py +0 -0
  92. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/tools/_helpers.py +0 -0
  93. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/tools/agent.py +0 -0
  94. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/tools/artifacts.py +0 -0
  95. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/tools/bitbucket.py +0 -0
  96. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/tools/credentials.py +0 -0
  97. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/tools/delegate.py +0 -0
  98. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/tools/edits.py +0 -0
  99. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/tools/federate.py +0 -0
  100. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/tools/jira.py +0 -0
  101. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/tools/memory.py +0 -0
  102. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/tools/project.py +0 -0
  103. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/tools/search.py +0 -0
  104. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/tools/session.py +0 -0
  105. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/tools/shell.py +0 -0
  106. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/tools/status.py +0 -0
  107. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/tools/tasks.py +0 -0
  108. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/ui/api.js +0 -0
  109. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/ui/app.js +0 -0
  110. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/ui/components/access.js +0 -0
  111. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/ui/components/bitbucket.js +0 -0
  112. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/ui/components/chat.js +0 -0
  113. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/ui/components/credentials.js +0 -0
  114. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/ui/components/dashboard.js +0 -0
  115. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/ui/components/edits.js +0 -0
  116. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/ui/components/enforcement.js +0 -0
  117. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/ui/components/instructions.js +0 -0
  118. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/ui/components/jira.js +0 -0
  119. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/ui/components/memory.js +0 -0
  120. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/ui/components/sessions.js +0 -0
  121. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/ui/components/settings.js +0 -0
  122. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/ui/components/sidebar.js +0 -0
  123. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/ui/components/tasks.js +0 -0
  124. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/ui/icons.js +0 -0
  125. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/ui/pm_shared.js +0 -0
  126. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/ui/shared.js +0 -0
  127. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/ui/theme.js +0 -0
  128. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/ui.html +0 -0
  129. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/ui_legacy.html +0 -0
  130. {code_context_control-2.71.0 → code_context_control-2.74.0}/cli/ui_nano.html +0 -0
  131. {code_context_control-2.71.0 → code_context_control-2.74.0}/code_context_control.egg-info/dependency_links.txt +0 -0
  132. {code_context_control-2.71.0 → code_context_control-2.74.0}/code_context_control.egg-info/entry_points.txt +0 -0
  133. {code_context_control-2.71.0 → code_context_control-2.74.0}/code_context_control.egg-info/requires.txt +0 -0
  134. {code_context_control-2.71.0 → code_context_control-2.74.0}/code_context_control.egg-info/top_level.txt +0 -0
  135. {code_context_control-2.71.0 → code_context_control-2.74.0}/core/__init__.py +0 -0
  136. {code_context_control-2.71.0 → code_context_control-2.74.0}/core/config.py +0 -0
  137. {code_context_control-2.71.0 → code_context_control-2.74.0}/core/ide.py +0 -0
  138. {code_context_control-2.71.0 → code_context_control-2.74.0}/core/mcp_toml.py +0 -0
  139. {code_context_control-2.71.0 → code_context_control-2.74.0}/core/web_security.py +0 -0
  140. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/__init__.py +0 -0
  141. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/config.py +0 -0
  142. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/mcp_oracle.py +0 -0
  143. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/oracle_server.py +0 -0
  144. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/oracle_ui.html +0 -0
  145. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/services/__init__.py +0 -0
  146. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/services/activity_reporter.py +0 -0
  147. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/services/api_auth.py +0 -0
  148. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/services/c3_bridge.py +0 -0
  149. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/services/chat_engine.py +0 -0
  150. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/services/chat_store.py +0 -0
  151. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/services/cross_memory.py +0 -0
  152. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/services/discovery_audit.py +0 -0
  153. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/services/federated_graph.py +0 -0
  154. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/services/health_checker.py +0 -0
  155. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/services/insight_engine.py +0 -0
  156. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/services/local_session.py +0 -0
  157. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/services/memory_reader.py +0 -0
  158. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/services/memory_writer.py +0 -0
  159. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/services/project_scanner.py +0 -0
  160. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/services/review_agent.py +0 -0
  161. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/services/tool_executor.py +0 -0
  162. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/services/tool_registry.py +0 -0
  163. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/ui/activity.js +0 -0
  164. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/ui/agents.js +0 -0
  165. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/ui/app.js +0 -0
  166. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/ui/busy.js +0 -0
  167. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/ui/chat/conversations.js +0 -0
  168. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/ui/chat/input.js +0 -0
  169. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/ui/chat/markdown.js +0 -0
  170. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/ui/chat/send.js +0 -0
  171. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/ui/chat/stream_renderer.js +0 -0
  172. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/ui/chat/toolbar.js +0 -0
  173. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/ui/core.js +0 -0
  174. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/ui/crossgraph.js +0 -0
  175. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/ui/header.js +0 -0
  176. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/ui/insights.js +0 -0
  177. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/ui/projects.js +0 -0
  178. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/ui/qrcode.js +0 -0
  179. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/ui/settings.js +0 -0
  180. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/ui/suggestions.js +0 -0
  181. {code_context_control-2.71.0 → code_context_control-2.74.0}/oracle/ui/theme_tabs.js +0 -0
  182. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/__init__.py +0 -0
  183. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/access_telemetry.py +0 -0
  184. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/activity_log.py +0 -0
  185. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/agent_base.py +0 -0
  186. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/agent_locks.py +0 -0
  187. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/agents.py +0 -0
  188. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/artifact_defs.py +0 -0
  189. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/artifact_store.py +0 -0
  190. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/auto_memory.py +0 -0
  191. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/bench/__init__.py +0 -0
  192. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/bench/external/__init__.py +0 -0
  193. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/bench/external/aider_polyglot.py +0 -0
  194. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/bench/external/swe_bench.py +0 -0
  195. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/benchmark_dashboard.py +0 -0
  196. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/bitbucket_client.py +0 -0
  197. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/bitbucket_credentials.py +0 -0
  198. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/circuit_breaker.py +0 -0
  199. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/claude_md.py +0 -0
  200. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/compressor.py +0 -0
  201. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/context_snapshot.py +0 -0
  202. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/conversation_store.py +0 -0
  203. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/credential_store.py +0 -0
  204. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/doc_index.py +0 -0
  205. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/e2e_benchmark.py +0 -0
  206. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/e2e_evaluator.py +0 -0
  207. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/e2e_tasks.py +0 -0
  208. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/edit_ledger.py +0 -0
  209. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/embedding_index.py +0 -0
  210. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/enforcement_policy.py +0 -0
  211. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/error_reporting.py +0 -0
  212. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/file_memory.py +0 -0
  213. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/git_context.py +0 -0
  214. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/hub_service.py +0 -0
  215. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/indexer.py +0 -0
  216. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/jira_client.py +0 -0
  217. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/jira_cloud.py +0 -0
  218. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/jira_credentials.py +0 -0
  219. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/jira_data_center.py +0 -0
  220. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/jira_links.py +0 -0
  221. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/mask_activation.py +0 -0
  222. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/mask_mirror.py +0 -0
  223. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/mask_presets.py +0 -0
  224. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/memory.py +0 -0
  225. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/memory_consolidator.py +0 -0
  226. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/memory_distiller.py +0 -0
  227. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/memory_graph.py +0 -0
  228. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/memory_grounder.py +0 -0
  229. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/memory_queue.py +0 -0
  230. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/memory_scorer.py +0 -0
  231. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/metrics.py +0 -0
  232. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/ollama_bridge.py +0 -0
  233. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/ollama_client.py +0 -0
  234. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/ollama_credentials.py +0 -0
  235. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/output_filter.py +0 -0
  236. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/override_grants.py +0 -0
  237. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/parser.py +0 -0
  238. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/project_manager.py +0 -0
  239. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/project_runtime.py +0 -0
  240. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/protocol.py +0 -0
  241. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/proxy_state.py +0 -0
  242. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/repo_map.py +0 -0
  243. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/retention.py +0 -0
  244. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/retrieval_broker.py +0 -0
  245. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/router.py +0 -0
  246. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/runtime.py +0 -0
  247. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/scanner.py +0 -0
  248. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/session_benchmark.py +0 -0
  249. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/session_manager.py +0 -0
  250. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/session_preloader.py +0 -0
  251. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/subprojects.py +0 -0
  252. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/task_store.py +0 -0
  253. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/telemetry.py +0 -0
  254. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/text_index.py +0 -0
  255. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/time_tracker.py +0 -0
  256. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/tool_classifier.py +0 -0
  257. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/transcript_index.py +0 -0
  258. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/validation_cache.py +0 -0
  259. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/vector_store.py +0 -0
  260. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/watcher.py +0 -0
  261. {code_context_control-2.71.0 → code_context_control-2.74.0}/services/win_subprocess.py +0 -0
  262. {code_context_control-2.71.0 → code_context_control-2.74.0}/setup.cfg +0 -0
  263. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_access_builtin_optout.py +0 -0
  264. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_access_guard.py +0 -0
  265. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_access_guard_meta.py +0 -0
  266. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_access_guard_shell_project.py +0 -0
  267. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_access_guard_surfaces.py +0 -0
  268. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_access_guard_wiring.py +0 -0
  269. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_activity_reporter.py +0 -0
  270. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_agent_locks.py +0 -0
  271. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_aider_polyglot.py +0 -0
  272. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_antigravity_transition.py +0 -0
  273. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_artifact_store.py +0 -0
  274. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_artifact_tool.py +0 -0
  275. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_bitbucket_cli_smoke.py +0 -0
  276. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_bitbucket_client.py +0 -0
  277. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_bitbucket_config_fallback.py +0 -0
  278. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_bitbucket_credentials.py +0 -0
  279. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_bitbucket_tool.py +0 -0
  280. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_c3_bridge_project_artifacts.py +0 -0
  281. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_c3_shell.py +0 -0
  282. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_circuit_breaker.py +0 -0
  283. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_claude_md_merge.py +0 -0
  284. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_cli_smoke.py +0 -0
  285. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_compressor_large_file.py +0 -0
  286. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_credential_store.py +0 -0
  287. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_credentials_routes.py +0 -0
  288. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_credentials_tool.py +0 -0
  289. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_creds_cli_smoke.py +0 -0
  290. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_delegate_cascade.py +0 -0
  291. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_delegate_version_probe.py +0 -0
  292. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_e2e_benchmark.py +0 -0
  293. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_edit_ledger_hook.py +0 -0
  294. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_edit_locking.py +0 -0
  295. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_edit_normalization.py +0 -0
  296. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_embedding_index_deadlock.py +0 -0
  297. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_enforcement_flip.py +0 -0
  298. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_enforcement_policy.py +0 -0
  299. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_enforcement_routes.py +0 -0
  300. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_federated_graph.py +0 -0
  301. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_filter_backoff.py +0 -0
  302. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_ghost_files.py +0 -0
  303. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_ghost_generation.py +0 -0
  304. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_git_branch_awareness.py +0 -0
  305. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_hook_access_guard.py +0 -0
  306. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_hook_dispatch.py +0 -0
  307. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_hook_pretool_enforce.py +0 -0
  308. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_hook_prompt_recall.py +0 -0
  309. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_hook_smoke.py +0 -0
  310. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_hook_state.py +0 -0
  311. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_hook_utf8_stdio.py +0 -0
  312. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_hub_credentials_route.py +0 -0
  313. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_hub_credentials_routes_write.py +0 -0
  314. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_hub_enforcement_routes.py +0 -0
  315. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_hub_inspect_api.py +0 -0
  316. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_hub_locks_routes.py +0 -0
  317. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_hub_server_smoke.py +0 -0
  318. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_hub_subproject_links.py +0 -0
  319. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_ide_workflow_adaptation.py +0 -0
  320. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_init_scan.py +0 -0
  321. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_install_mcp_entrypoint.py +0 -0
  322. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_jira_cli_smoke.py +0 -0
  323. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_jira_client.py +0 -0
  324. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_jira_config_fallback.py +0 -0
  325. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_jira_credentials.py +0 -0
  326. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_jira_links.py +0 -0
  327. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_jira_routes.py +0 -0
  328. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_jira_tool.py +0 -0
  329. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_lazy_store_init.py +0 -0
  330. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_mask_guard.py +0 -0
  331. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_mask_routes.py +0 -0
  332. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_mask_surfaces.py +0 -0
  333. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_mcp_host_guard.py +0 -0
  334. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_mcp_memory_timeout.py +0 -0
  335. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_mcp_server_smoke.py +0 -0
  336. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_mcp_toml.py +0 -0
  337. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_memory_distiller.py +0 -0
  338. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_memory_graph_api.py +0 -0
  339. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_memory_queue.py +0 -0
  340. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_memory_system.py +0 -0
  341. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_mobile_security_api.py +0 -0
  342. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_no_stale_model_pins.py +0 -0
  343. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_notification_dedup.py +0 -0
  344. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_notification_discipline.py +0 -0
  345. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_ollama_credentials.py +0 -0
  346. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_oracle_api_auth.py +0 -0
  347. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_oracle_apikey_api.py +0 -0
  348. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_oracle_c3_bridge.py +0 -0
  349. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_oracle_chat_engine.py +0 -0
  350. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_oracle_chat_store.py +0 -0
  351. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_oracle_discovery_api.py +0 -0
  352. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_oracle_discovery_audit.py +0 -0
  353. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_oracle_local_auth.py +0 -0
  354. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_oracle_ollama_bridge.py +0 -0
  355. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_oracle_scanner_cache.py +0 -0
  356. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_oracle_security_fixes.py +0 -0
  357. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_oracle_session_bootstrap.py +0 -0
  358. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_oracle_subproject_awareness.py +0 -0
  359. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_oracle_ui_bundle.py +0 -0
  360. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_output_filter.py +0 -0
  361. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_override_grants.py +0 -0
  362. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_permissions.py +0 -0
  363. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_pm_api.py +0 -0
  364. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_project_manager.py +0 -0
  365. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_project_manager_merge.py +0 -0
  366. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_project_tool.py +0 -0
  367. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_read_coercion.py +0 -0
  368. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_read_edit_parity.py +0 -0
  369. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_related_facts.py +0 -0
  370. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_repo_map.py +0 -0
  371. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_response_boilerplate.py +0 -0
  372. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_retention.py +0 -0
  373. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_review_digest.py +0 -0
  374. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_service_durability.py +0 -0
  375. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_session_benchmark.py +0 -0
  376. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_session_budget.py +0 -0
  377. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_shell_creds.py +0 -0
  378. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_shell_robustness.py +0 -0
  379. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_subproject_exclusion.py +0 -0
  380. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_subproject_federation.py +0 -0
  381. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_subprojects.py +0 -0
  382. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_swe_bench.py +0 -0
  383. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_task_store.py +0 -0
  384. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_task_tool.py +0 -0
  385. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_time_tracker.py +0 -0
  386. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_token_telemetry.py +0 -0
  387. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_tool_registry.py +0 -0
  388. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_upgrade_and_version.py +0 -0
  389. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_validate.py +0 -0
  390. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_vault_write_guard.py +0 -0
  391. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_version_sync.py +0 -0
  392. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_web_security.py +0 -0
  393. {code_context_control-2.71.0 → code_context_control-2.74.0}/tests/test_windows_reliability.py +0 -0
  394. {code_context_control-2.71.0 → code_context_control-2.74.0}/tui/__init__.py +0 -0
  395. {code_context_control-2.71.0 → code_context_control-2.74.0}/tui/backend.py +0 -0
  396. {code_context_control-2.71.0 → code_context_control-2.74.0}/tui/main.py +0 -0
  397. {code_context_control-2.71.0 → code_context_control-2.74.0}/tui/screens/__init__.py +0 -0
  398. {code_context_control-2.71.0 → code_context_control-2.74.0}/tui/screens/benchmark_view.py +0 -0
  399. {code_context_control-2.71.0 → code_context_control-2.74.0}/tui/screens/claudemd_view.py +0 -0
  400. {code_context_control-2.71.0 → code_context_control-2.74.0}/tui/screens/compress_view.py +0 -0
  401. {code_context_control-2.71.0 → code_context_control-2.74.0}/tui/screens/index_view.py +0 -0
  402. {code_context_control-2.71.0 → code_context_control-2.74.0}/tui/screens/init_view.py +0 -0
  403. {code_context_control-2.71.0 → code_context_control-2.74.0}/tui/screens/mcp_view.py +0 -0
  404. {code_context_control-2.71.0 → code_context_control-2.74.0}/tui/screens/optimize_view.py +0 -0
  405. {code_context_control-2.71.0 → code_context_control-2.74.0}/tui/screens/pipe_view.py +0 -0
  406. {code_context_control-2.71.0 → code_context_control-2.74.0}/tui/screens/projects_view.py +0 -0
  407. {code_context_control-2.71.0 → code_context_control-2.74.0}/tui/screens/search_view.py +0 -0
  408. {code_context_control-2.71.0 → code_context_control-2.74.0}/tui/screens/session_view.py +0 -0
  409. {code_context_control-2.71.0 → code_context_control-2.74.0}/tui/screens/stats.py +0 -0
  410. {code_context_control-2.71.0 → code_context_control-2.74.0}/tui/screens/ui_view.py +0 -0
  411. {code_context_control-2.71.0 → code_context_control-2.74.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.71.0
3
+ Version: 2.74.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
@@ -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.71.0"
95
+ __version__ = "2.74.0"
96
96
 
97
97
 
98
98
  def _compress_file_cli(compressor, path, mode="smart", **kw):
@@ -0,0 +1,61 @@
1
+ """Override grants on the MCP content surface (override-requests.md P2a).
2
+
3
+ The hooks have consulted grants since P1. The `c3_*` content tools have not:
4
+ they called the evaluator and refused without ever asking whether the user
5
+ had already approved this exact call. §13's coverage matrix recorded that as
6
+ "not yet — P2a", which meant an approved request did nothing for any agent
7
+ working through MCP — the grant was minted, and then no one looked at it.
8
+
9
+ This module is the single place those tools ask. It is deliberately thin:
10
+ all the policy ordering, TTL clamping, use accounting and audit lives in
11
+ `services.override_grants.gate_access`, which is the same call the hooks
12
+ make. Two implementations of "may I" is how the two surfaces drift apart.
13
+
14
+ `session_id` lives here for the same reason. It was duplicated in edit.py,
15
+ locks.py and override.py, each carrying a comment saying it must match the
16
+ others exactly — and P2a makes that invariant load-bearing rather than
17
+ merely tidy: a grant is minted under the session id `c3_override` computed
18
+ and consumed under the one `c3_edit` computes. If those two ever disagree,
19
+ every approval silently fails to apply.
20
+ """
21
+
22
+ import os
23
+
24
+
25
+ def session_id(svc) -> str:
26
+ """This agent's identity, for leases and for grants.
27
+
28
+ Falls back to the process id, never to "". Two agents that both resolved
29
+ to "" would count as ONE session and stop blocking each other — the exact
30
+ opposite of what a lock is for. Each Claude Code session runs its own
31
+ c3-mcp process, so the pid is a faithful stand-in.
32
+
33
+ It is also the right identity for c3_project: that proxy builds a runtime
34
+ for the TARGET project but runs inside the CALLER's process, so the pid
35
+ keeps the edit attributed to the agent that actually asked for it.
36
+ """
37
+ session = getattr(getattr(svc, "session_mgr", None), "current_session", None) or {}
38
+ return str(session.get("id", "") or "") or f"pid-{os.getpid()}"
39
+
40
+
41
+ def allow(svc, denial, *, tool: str, op: str, path) -> str | None:
42
+ """The `[c3-override:granted]` line when a live grant permits this exact
43
+ call, or None to stay denied.
44
+
45
+ Consumes a use on success — so callers must invoke this only when they
46
+ are about to proceed, never speculatively. Ordering inside `gate_access`
47
+ is policy first, grants second, so a grant is void the instant the policy
48
+ or its layer is switched off (§12.8).
49
+
50
+ Fail-closed by construction: any error resolving the grant store leaves
51
+ the caller on its ordinary refusal path. A grant that cannot be read is
52
+ not a grant.
53
+ """
54
+ if denial is None:
55
+ return None
56
+ try:
57
+ from services import override_grants as og # noqa: PLC0415 — lazy
58
+ return og.gate_access(svc.project_path, denial, tool=tool, op=op,
59
+ path=str(path), session_id=session_id(svc))
60
+ except Exception:
61
+ return None
@@ -8,6 +8,7 @@ import sys
8
8
  from concurrent.futures import ThreadPoolExecutor, as_completed
9
9
  from pathlib import Path
10
10
 
11
+ from cli.tools import _grants
11
12
  from cli.tools._helpers import finalize_with_tokens, show_token_ratios
12
13
  from core import count_tokens
13
14
  from services import access_guard
@@ -118,7 +119,9 @@ def _compress_single(file_path: str, mode: str, svc, finalize, maybe_facts) -> s
118
119
  # Access Guard: read verdict up front — covers the map/dense_map paths
119
120
  # that never reach compressor.compress_file (docs/access-guard.md §3).
120
121
  _verdict = access_guard.verdict(file_path, "read", svc.project_path)
121
- if _verdict.denial:
122
+ if _verdict.denial and not _grants.allow(svc, _verdict.denial,
123
+ tool="c3_compress", op="read",
124
+ path=file_path):
122
125
  return finalize("c3_compress", {"file_path": file_path, "mode": mode},
123
126
  access_guard.refusal(_verdict.denial, file_path, "read"),
124
127
  "access-denied")
@@ -22,25 +22,22 @@ import threading
22
22
  from contextlib import contextmanager
23
23
  from pathlib import Path
24
24
 
25
+ from cli.tools import _grants
25
26
  from services import access_guard, agent_locks
26
27
  from services import credential_store as _cs
27
28
  from services.task_store import _FileLock
28
29
 
29
30
 
30
31
  def _session_id(svc) -> str:
31
- """This agent's lease identity.
32
+ """This agent's lease identity — see `cli.tools._grants.session_id`.
32
33
 
33
- Falls back to the process id, never to "". Two agents that both resolved
34
- to "" would count as ONE session and stop blocking each other — the exact
35
- opposite of what a lock is for. Each Claude Code session runs its own
36
- c3-mcp process, so the pid is a faithful stand-in.
37
-
38
- It is also the right identity for c3_project: that proxy builds a runtime
39
- for the TARGET project but runs inside the CALLER's process, so the pid
40
- keeps the edit attributed to the agent that actually asked for it.
34
+ Was defined here and copied into locks.py and override.py, each with a
35
+ comment saying it had to match. P2a makes that invariant load-bearing:
36
+ a grant is minted under the id c3_override computes and consumed under
37
+ the one c3_edit computes, so a divergence would make every approval
38
+ silently fail to apply. One definition, three callers.
41
39
  """
42
- session = getattr(getattr(svc, "session_mgr", None), "current_session", None) or {}
43
- return str(session.get("id", "") or "") or f"pid-{os.getpid()}"
40
+ return _grants.session_id(svc)
44
41
 
45
42
  # ── Same-file serialization ───────────────────────────────────────────────
46
43
  # In-process locks, keyed by resolved absolute path string.
@@ -325,7 +322,8 @@ def handle_edit(file_path: str, old_string: str, new_string: str,
325
322
  # (never replaces) any dedicated vault-file guard.
326
323
  op = "write" if path.exists() else "create"
327
324
  denial = access_guard.check(str(path), op, svc.project_path)
328
- if denial:
325
+ if denial and not _grants.allow(svc, denial, tool="c3_edit", op=op,
326
+ path=str(path)):
329
327
  return finalize("c3_edit", {"file": file_path},
330
328
  access_guard.refusal(denial, file_path, op),
331
329
  "access-denied")
@@ -10,6 +10,7 @@ import json
10
10
  import re
11
11
  from pathlib import Path
12
12
 
13
+ from cli.tools import _grants
13
14
  from cli.tools._helpers import finalize_with_tokens, show_token_ratios
14
15
  from core import count_tokens
15
16
  from services import access_guard
@@ -31,7 +32,9 @@ def handle_filter(file_path: str, text: str, pattern: str, max_lines: int,
31
32
  # Access Guard: read verdict (docs/access-guard.md §3), checked before
32
33
  # existence so probes can't distinguish missing from denied (R2).
33
34
  denial = access_guard.check(file_path, "read", svc.project_path)
34
- if denial:
35
+ _granted = _grants.allow(svc, denial, tool="c3_filter", op="read",
36
+ path=file_path) if denial else None
37
+ if denial and not _granted:
35
38
  return finalize("c3_filter", {"file": file_path},
36
39
  access_guard.refusal(denial, file_path, "read"),
37
40
  "access-denied")
@@ -9,6 +9,7 @@ import subprocess
9
9
  import sys
10
10
  from pathlib import Path
11
11
 
12
+ from cli.tools import _grants
12
13
  from services import access_guard
13
14
 
14
15
  _SKIP_DIRS = frozenset({
@@ -115,7 +116,8 @@ def handle_impact(target: str, file_path: str, mode: str, svc, finalize) -> str:
115
116
  # Access Guard: refuse outright when the named source file is read-denied.
116
117
  if file_path:
117
118
  denial = access_guard.check(file_path, "read", str(project))
118
- if denial:
119
+ if denial and not _grants.allow(svc, denial, tool="c3_impact",
120
+ op="read", path=file_path):
119
121
  return finalize("c3_impact", {"target": target, "mode": mode},
120
122
  access_guard.refusal(denial, file_path, "read"),
121
123
  "access-denied")
@@ -12,17 +12,14 @@ and letting go early instead of waiting out the TTL.
12
12
  force_release is deliberately absent: it is a human override that bumps the
13
13
  fencing counter, and it lives in `c3 locks force-release` / the Hub tab.
14
14
  """
15
- import os
16
-
15
+ from cli.tools import _grants
17
16
  from services import agent_locks as al
18
17
 
19
18
 
20
19
  def _session_id(svc) -> str:
21
- """Must match cli/tools/edit.py._session_id exactly a lease taken by
22
- c3_edit has to be recognised as ours by c3_locks, and vice versa. Never ""
23
- (see the note there: two anonymous agents would stop blocking each other)."""
24
- session = getattr(getattr(svc, "session_mgr", None), "current_session", None) or {}
25
- return str(session.get("id", "") or "") or f"pid-{os.getpid()}"
20
+ """A lease taken by c3_edit has to be recognised as ours by c3_locks, and
21
+ vice versa so both read the one definition in `_grants`."""
22
+ return _grants.session_id(svc)
26
23
 
27
24
 
28
25
  def _split(paths: str) -> list:
@@ -12,9 +12,9 @@ Layers that can never be escalated (the credential vault, Tier-0 denies, the
12
12
  dispatcher fail-closed deny, catastrophic shell blocks) are refused at
13
13
  creation with `[c3-override:not-escalatable]` and never reach a human.
14
14
  """
15
- import os
16
15
  import time
17
16
 
17
+ from cli.tools import _grants
18
18
  from services import access_guard as ag
19
19
  from services import override_policy as opol
20
20
  from services import override_requests as orq
@@ -28,9 +28,8 @@ _DEFAULT_TOOL = {"read": "Read", "write": "Edit"}
28
28
 
29
29
 
30
30
  def _session_id(svc) -> str:
31
- """Must match cli/tools/edit.py._session_id — the grant is bound to it."""
32
- session = getattr(getattr(svc, "session_mgr", None), "current_session", None) or {}
33
- return str(session.get("id", "") or "") or f"pid-{os.getpid()}"
31
+ """The identity the grant is bound to — one definition, in `_grants`."""
32
+ return _grants.session_id(svc)
34
33
 
35
34
 
36
35
  def _fmt(row: dict) -> str:
@@ -6,6 +6,7 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
6
6
  from pathlib import Path
7
7
  from typing import Any
8
8
 
9
+ from cli.tools import _grants
9
10
  from cli.tools._helpers import finalize_with_tokens, maybe_related_facts
10
11
  from core import count_tokens
11
12
  from services import access_guard
@@ -123,8 +124,14 @@ def handle_read(file_path: str, symbols: Any = None, lines: Any = None,
123
124
  # before existence so probes get the same refusal whether or not the
124
125
  # target exists (R2). Batch members hit this via the per-file dispatch,
125
126
  # so allowed members are served and denied ones carry the S1 line inline.
127
+ # A live grant (P2a) is consulted only on the DENIAL branch. The masked
128
+ # branch below is untouched on purpose: a mask is not a refusal to be
129
+ # lifted, it is a different view being served, and "approve once" has no
130
+ # meaning for it. Widening that is a separate decision, not a side effect.
126
131
  _verdict = access_guard.verdict(file_path, "read", svc.project_path)
127
- if _verdict.denial:
132
+ if _verdict.denial and not _grants.allow(svc, _verdict.denial,
133
+ tool="c3_read", op="read",
134
+ path=file_path):
128
135
  resp = access_guard.refusal(_verdict.denial, file_path, "read")
129
136
  if finalize is None:
130
137
  return resp
@@ -8,6 +8,7 @@ import subprocess
8
8
  import sys
9
9
  from pathlib import Path
10
10
 
11
+ from cli.tools import _grants
11
12
  from services import access_guard
12
13
 
13
14
 
@@ -169,7 +170,8 @@ async def _validate_one(file_path: str, svc) -> tuple:
169
170
  # Access Guard: read verdict (docs/access-guard.md §3) — the S1 refusal
170
171
  # becomes the batch line for this member; other members are still served.
171
172
  denial = access_guard.check(file_path, "read", svc.project_path)
172
- if denial:
173
+ if denial and not _grants.allow(svc, denial, tool="c3_validate",
174
+ op="read", path=file_path):
173
175
  return ("SKIP", access_guard.refusal(denial, file_path, "read"))
174
176
  full = Path(svc.project_path) / file_path
175
177
  if not full.exists():
@@ -250,7 +252,8 @@ async def _validate_single(file_path: str, svc, finalize) -> str:
250
252
  """Original single-file validation path."""
251
253
  # Access Guard: read verdict, checked before existence (R2 probe parity).
252
254
  denial = access_guard.check(file_path, "read", svc.project_path)
253
- if denial:
255
+ if denial and not _grants.allow(svc, denial, tool="c3_validate",
256
+ op="read", path=file_path):
254
257
  return finalize("c3_validate", {"file_path": file_path},
255
258
  access_guard.refusal(denial, file_path, "read"),
256
259
  "access-denied")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-context-control
3
- Version: 2.71.0
3
+ Version: 2.74.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
@@ -69,6 +69,7 @@ cli/hub_ui/components/task_board.js
69
69
  cli/hub_ui/components/toasts.js
70
70
  cli/hub_ui/components/topbar.js
71
71
  cli/tools/__init__.py
72
+ cli/tools/_grants.py
72
73
  cli/tools/_helpers.py
73
74
  cli/tools/agent.py
74
75
  cli/tools/artifacts.py
@@ -222,6 +223,7 @@ services/output_filter.py
222
223
  services/override_grants.py
223
224
  services/override_policy.py
224
225
  services/override_requests.py
226
+ services/override_wake.py
225
227
  services/parser.py
226
228
  services/project_manager.py
227
229
  services/project_runtime.py
@@ -352,7 +354,9 @@ tests/test_oracle_subproject_awareness.py
352
354
  tests/test_oracle_ui_bundle.py
353
355
  tests/test_output_filter.py
354
356
  tests/test_override_grants.py
357
+ tests/test_override_grants_mcp.py
355
358
  tests/test_override_requests.py
359
+ tests/test_override_wake.py
356
360
  tests/test_permissions.py
357
361
  tests/test_pm_api.py
358
362
  tests/test_project_manager.py
@@ -46,6 +46,7 @@ import hashlib
46
46
  import json
47
47
  import os
48
48
  import threading
49
+ import time
49
50
  from datetime import datetime, timezone
50
51
  from pathlib import Path
51
52
 
@@ -58,10 +59,13 @@ from oracle.services.tool_registry import _c3_version
58
59
 
59
60
  # 2: /info reports EFFECTIVE capabilities (filtered by the config switches)
60
61
  # rather than a static list, so a client can gate its UI instead of probing.
61
- API_VERSION = 2
62
+ # 3: /feed accepts `wait` (long-poll). Advertised as the `feed_wait`
63
+ # capability so a client can choose to hold a connection open instead of
64
+ # polling, rather than discovering support by timing a request.
65
+ API_VERSION = 3
62
66
 
63
67
  CAPABILITIES = [
64
- "feed", "projects", "health", "pm", "pm_events", "digest",
68
+ "feed", "feed_wait", "projects", "health", "pm", "pm_events", "digest",
65
69
  "notifications_ack",
66
70
  "credentials", "credentials_write",
67
71
  "access", "access_write",
@@ -90,6 +94,38 @@ _SOURCE_CAP = 1000
90
94
  _MAX_LIMIT = 200
91
95
  _DEFAULT_LIMIT = 50
92
96
 
97
+ # ── Long-poll (`/feed?wait=`) ─────────────────────────────
98
+ #
99
+ # WHY: the phone's only delivery paths were two pollers — every 15s while the
100
+ # app is open, and Android WorkManager's ~15-MINUTE floor once it is closed.
101
+ # An override request with a 10-minute TTL could therefore expire before the
102
+ # phone was ever told it existed (observed 2026-08-07). `wait` lets a client
103
+ # hold one request open and be answered the moment something lands, turning
104
+ # "up to 15 seconds" into "about a second" without spending a request every
105
+ # 15s to get it.
106
+ #
107
+ # It is NOT push, and nothing here should be read as a claim that it is: a
108
+ # process Android has frozen cannot hold a socket. This narrows the live
109
+ # window. FCM or a foreground service is what would close the closed-app one.
110
+ _MAX_WAIT_S = 30
111
+ # Re-scan cadence while holding. The scan is skipped unless an mtime moved,
112
+ # so this is a stat() per second, not a feed rebuild per second.
113
+ _WAIT_TICK_S = 1.0
114
+ # Each waiter parks a server thread for up to _MAX_WAIT_S, and the Oracle
115
+ # serves the desktop UI and discovery off the same pool. Past the cap `wait`
116
+ # degrades to an ordinary immediate answer rather than queueing behind other
117
+ # waiters. One phone needs one slot; four leaves room for the watch and a
118
+ # spare.
119
+ _MAX_WAITERS = 4
120
+ _waiters = threading.Semaphore(_MAX_WAITERS)
121
+
122
+ #: Files whose mtime means "the feed may have changed" — the same set
123
+ #: ``_last_activity`` reports on, kept literal in both places on purpose. Here
124
+ #: it is a correctness input: a file missing from this tuple is a wake that
125
+ #: never returns early.
126
+ _FEED_FILES = ("activity_log.jsonl", "edit_ledger.jsonl",
127
+ "notifications.jsonl", "pm/pm.json")
128
+
93
129
  bp = Blueprint("mobile", __name__, url_prefix="/api/mobile")
94
130
 
95
131
  # Wired by oracle_server at import time (configure) and startup (init_services).
@@ -489,6 +525,54 @@ def _feed_items_for(path: str, name: str, types: set, severities: set,
489
525
  return items
490
526
 
491
527
 
528
+ def _feed_mtime(targets: list) -> float:
529
+ """Newest mtime across every target's feed files. 0.0 when none exist."""
530
+ latest = 0.0
531
+ for t in targets:
532
+ c3 = Path(t["path"]) / ".c3"
533
+ for rel in _FEED_FILES:
534
+ try:
535
+ latest = max(latest, (c3 / rel).stat().st_mtime)
536
+ except OSError:
537
+ continue
538
+ return latest
539
+
540
+
541
+ def _hold_for_items(targets: list, collect, wait_s: int):
542
+ """Block up to *wait_s* for the feed to produce something.
543
+
544
+ Returns ``(items, waited_seconds)``. Rebuilding the feed every tick would
545
+ be a full history read per target, so the loop watches mtimes and only
546
+ pays for a rebuild when one moves. An mtime that moves without producing a
547
+ matching item — a severity this client filters out, say — re-baselines
548
+ instead of re-scanning on every subsequent tick.
549
+
550
+ Returns immediately when every waiter slot is taken. The caller then
551
+ behaves exactly as if ``wait`` had not been passed: a slower client, not a
552
+ wrong one.
553
+ """
554
+ started = time.monotonic()
555
+ if not _waiters.acquire(blocking=False):
556
+ return [], 0.0
557
+ try:
558
+ seen = _feed_mtime(targets)
559
+ deadline = started + wait_s
560
+ while True:
561
+ remaining = deadline - time.monotonic()
562
+ if remaining <= 0:
563
+ return [], time.monotonic() - started
564
+ time.sleep(min(_WAIT_TICK_S, remaining))
565
+ current = _feed_mtime(targets)
566
+ if current == seen:
567
+ continue
568
+ seen = current
569
+ found = collect()
570
+ if found:
571
+ return found, time.monotonic() - started
572
+ finally:
573
+ _waiters.release()
574
+
575
+
492
576
  @bp.route("/feed")
493
577
  def mobile_feed():
494
578
  """Merged cross-project feed, newest first.
@@ -496,11 +580,14 @@ def mobile_feed():
496
580
  Query: types (csv of notification|activity|edit|session_stat, default
497
581
  all), project (optional single project), severity (csv, notifications
498
582
  only), since (exclusive watermark — "only newer than"), before
499
- (exclusive pagination cursor), limit (default 50, max 200).
583
+ (exclusive pagination cursor), limit (default 50, max 200), wait
584
+ (seconds, 0-30 — hold the request open until something matches).
500
585
 
501
586
  Response: {items, next_cursor, truncated}. Cursor is the ts of the last
502
587
  returned item; identical-ts items can repeat across pages, so clients
503
- dedup by item id.
588
+ dedup by item id. A `wait` request also carries `waited_s`, and an empty
589
+ `items` after a full hold is the normal, expected answer — the client
590
+ reconnects with the same watermark.
504
591
  """
505
592
  if _scanner is None:
506
593
  return jsonify({"error": "not initialized"}), 500
@@ -523,6 +610,18 @@ def mobile_feed():
523
610
  except (TypeError, ValueError):
524
611
  limit = _DEFAULT_LIMIT
525
612
 
613
+ try:
614
+ wait_s = max(0, min(_MAX_WAIT_S, int(request.args.get("wait", 0))))
615
+ except (TypeError, ValueError):
616
+ wait_s = 0
617
+ # Holding open only makes sense against a watermark. Without `since` the
618
+ # first scan matches history and returns instantly, so a client that
619
+ # forgot it would silently get the old behaviour and never learn why its
620
+ # push felt slow; with `before` this is a pagination cursor, where waiting
621
+ # is meaningless.
622
+ if before or not since:
623
+ wait_s = 0
624
+
526
625
  project_arg = (request.args.get("project") or "").strip()
527
626
  if project_arg:
528
627
  resolved, err = _project_or_404(project_arg)
@@ -536,10 +635,17 @@ def mobile_feed():
536
635
  if p.get("has_c3") and p.get("path")
537
636
  and (Path(p["path"]) / ".c3").is_dir()]
538
637
 
539
- items: list[dict] = []
540
- for t in targets:
541
- items.extend(_feed_items_for(t["path"], t["name"], types, severities,
542
- since, before))
638
+ def collect() -> list:
639
+ out: list[dict] = []
640
+ for t in targets:
641
+ out.extend(_feed_items_for(t["path"], t["name"], types,
642
+ severities, since, before))
643
+ return out
644
+
645
+ items = collect()
646
+ waited = 0.0
647
+ if not items and wait_s:
648
+ items, waited = _hold_for_items(targets, collect, wait_s)
543
649
 
544
650
  items.sort(key=lambda i: i["ts"], reverse=True)
545
651
  truncated = len(items) > limit
@@ -548,6 +654,9 @@ def mobile_feed():
548
654
  "items": page,
549
655
  "next_cursor": page[-1]["ts"] if truncated and page else None,
550
656
  "truncated": truncated,
657
+ # Only when the client asked to hold, so the response shape is
658
+ # byte-identical for every client that did not.
659
+ **({"waited_s": round(waited, 2)} if wait_s else {}),
551
660
  })
552
661
 
553
662
 
@@ -1634,6 +1743,10 @@ def mobile_enforcement_set():
1634
1743
  # (e.g. `path_key`) cannot silently start crossing the network.
1635
1744
 
1636
1745
  #: §3.3 field order, verbatim. The mobile client reads these names.
1746
+ #: The one `override` key no remote surface may write. See the refusal in
1747
+ #: `mobile_overrides_policy_set` for why it is a 403 rather than a 400.
1748
+ _WAKE_KEY = "wake"
1749
+
1637
1750
  _OVERRIDE_FIELDS = (
1638
1751
  "id", "project_path", "session_id", "created_at", "expires_at", "status",
1639
1752
  "layer", "rule", "rule_class", "scope", "tool", "op", "path", "refusal",
@@ -1809,6 +1922,17 @@ def mobile_overrides_policy_set():
1809
1922
  section = data.get("override")
1810
1923
  if not isinstance(section, dict):
1811
1924
  return jsonify({"error": "body needs an 'override' object"}), 400
1925
+ if _WAKE_KEY in section:
1926
+ # `wake` is an argv this machine will execute. Everything else on this
1927
+ # route widens what a tap can approve; this one would decide what runs
1928
+ # when it does. A bearer token from a phone is authentication, not
1929
+ # physical presence, so it stays a desktop edit to a human-owned file.
1930
+ return jsonify({
1931
+ "error": "'wake' cannot be set from the phone — it names a "
1932
+ "command this machine runs. Edit .c3/config.json on the "
1933
+ "desktop.",
1934
+ "key": _WAKE_KEY,
1935
+ }), 403
1812
1936
  unknown = sorted(set(section) - set(opol.DEFAULTS))
1813
1937
  if unknown:
1814
1938
  # §3.1: unknown keys are a hard error, never a silent no-op.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "code-context-control"
7
- version = "2.71.0"
7
+ version = "2.74.0"
8
8
  description = "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."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -709,11 +709,12 @@ def _override_offer(denial: Denial, path, operation: str, tool: str) -> str:
709
709
  that can never be escalated: an agent must not learn from a refusal that
710
710
  a request surface exists for the credential vault (§6).
711
711
 
712
- Restricted to the hook surface on purpose. The offer promises that a human
713
- 'yes' makes the retry work, and today only the PreToolUse gates consult
714
- grants — the ``c3_*`` content surfaces do not (see §13, phase P2a). An
715
- offer on a surface that would still refuse after approval is worse than
716
- no offer.
712
+ Restricted to the hook surface on purpose see ``refusal``. The rule was
713
+ written when only the PreToolUse gates consulted grants; P2a taught the
714
+ ``c3_*`` content surfaces to consult them too, so extending the offer to
715
+ the MCP surface is now defensible. It is deliberately NOT done here: that
716
+ is a separate, wider change (every c3_* refusal grows a line) and belongs
717
+ in its own review, not smuggled in beside a fix for the branch ordering.
717
718
  """
718
719
  try:
719
720
  from services import override_policy as _op # noqa: PLC0415 — cycle
@@ -738,7 +739,26 @@ def refusal(denial: Denial, path, operation: str, *, surface: str = "mcp",
738
739
  the agent it may ASK (docs/override-requests.md §6). The pinned strings
739
740
  above are unchanged: the append is absent unless the project opted in,
740
741
  which is off by default.
742
+
743
+ The append happens HERE, once, for every kind — not inside a branch. It
744
+ used to live at the tail of the deny branch, which sits below the mask and
745
+ read-only early returns, so those two kinds could never carry an offer on
746
+ any surface even though `rule_class_for_denial` marks both escalatable.
747
+ `access_readonly` was the only layer anyone had turned on, so in practice
748
+ the invitation half of Override Requests had never fired once.
741
749
  """
750
+ body = _refusal_body(denial, path, operation, surface=surface,
751
+ tool=tool, project=project)
752
+ # Hook only, still: the offer promises that a 'yes' makes the retry work,
753
+ # and the proxy surface answers for a DIFFERENT project's policy.
754
+ if surface == "hook":
755
+ return body + _override_offer(denial, path, operation, tool)
756
+ return body
757
+
758
+
759
+ def _refusal_body(denial: Denial, path, operation: str, *, surface: str,
760
+ tool: str, project: str) -> str:
761
+ """The pinned string itself, with no override append."""
742
762
  p, glob, scope = _cap(path), denial.rule, denial.scope
743
763
  if denial.kind == _KIND_MASK:
744
764
  if operation in ("write", "create", "delete"):
@@ -762,12 +782,20 @@ def refusal(denial: Denial, path, operation: str, *, surface: str = "mcp",
762
782
  "`c3 access list` or the Access tab."
763
783
  )
764
784
  if denial.kind == _KIND_READ_ONLY:
785
+ # `{operation}`, not a hardcoded "write". A read-only rule blocks the
786
+ # whole write class, and for a file that does not exist yet the tools
787
+ # call that operation `create`. Saying "write" told the agent to ask
788
+ # the user to approve `op='write'`, the approval was minted for
789
+ # `write`, and the grant matcher — which compares op exactly — then
790
+ # refused the `create` the tool actually attempted. A real approval
791
+ # was spent and silently discarded (§4 near_miss).
765
792
  return (
766
- f"{TAG_READ_ONLY} write denied for {p} by Access Guard rule "
793
+ f"{TAG_READ_ONLY} {operation} denied for {p} by Access Guard rule "
767
794
  f"'{glob}' ({scope} scope). The effective policy is read-only; "
768
- "reads are evaluated separately. Do not retry the write. Mark "
769
- "the affected step blocked and continue with unaffected files; "
770
- "report the skip. Rules: `c3 access list` or the Access tab."
795
+ f"reads are evaluated separately. Do not retry the {operation}. "
796
+ "Mark the affected step blocked and continue with unaffected "
797
+ "files; report the skip. Rules: `c3 access list` or the Access "
798
+ "tab."
771
799
  )
772
800
  if surface == "hook":
773
801
  return (
@@ -776,7 +804,7 @@ def refusal(denial: Denial, path, operation: str, *, surface: str = "mcp",
776
804
  "decision, not a transient error — do not retry through another "
777
805
  "tool or the shell. Mark the affected step blocked and continue "
778
806
  "with unaffected files. Rules: `c3 access list`."
779
- ) + _override_offer(denial, path, operation, tool)
807
+ )
780
808
  if surface == "proxy":
781
809
  return (
782
810
  f"{TAG_DENIED} {operation} denied for {p} through project "