code-puppy 0.0.783__tar.gz → 0.0.785__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 (272) hide show
  1. {code_puppy-0.0.783 → code_puppy-0.0.785}/PKG-INFO +2 -2
  2. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/callbacks.py +40 -0
  3. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/add_model_menu.py +19 -1
  4. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/mcp/trust_command.py +5 -2
  5. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/config.py +10 -1
  6. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/mcp_/manager.py +17 -6
  7. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/plugins/__init__.py +19 -14
  8. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/session_surrogate_unpickler.py +3 -11
  9. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/file_modifications.py +94 -0
  10. {code_puppy-0.0.783 → code_puppy-0.0.785}/pyproject.toml +2 -2
  11. {code_puppy-0.0.783 → code_puppy-0.0.785}/.gitignore +0 -0
  12. {code_puppy-0.0.783 → code_puppy-0.0.785}/LICENSE +0 -0
  13. {code_puppy-0.0.783 → code_puppy-0.0.785}/README.md +0 -0
  14. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/__init__.py +0 -0
  15. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/__main__.py +0 -0
  16. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/agent_execution_context.py +0 -0
  17. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/agents/__init__.py +0 -0
  18. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/agents/_builder.py +0 -0
  19. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/agents/_compaction.py +0 -0
  20. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/agents/_diagnostics.py +0 -0
  21. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/agents/_history.py +0 -0
  22. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/agents/_key_listeners.py +0 -0
  23. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/agents/_model_message_transform.py +0 -0
  24. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/agents/_non_streaming_render.py +0 -0
  25. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/agents/_output_limits.py +0 -0
  26. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/agents/_run_signals.py +0 -0
  27. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/agents/_runtime.py +0 -0
  28. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/agents/_steer_processor.py +0 -0
  29. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/agents/_subagent_recursion.py +0 -0
  30. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/agents/agent_code_puppy.py +0 -0
  31. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/agents/agent_creator_agent.py +0 -0
  32. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/agents/agent_helios.py +0 -0
  33. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/agents/agent_manager.py +0 -0
  34. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/agents/agent_model_judge.py +0 -0
  35. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/agents/agent_planning.py +0 -0
  36. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/agents/agent_qa_kitten.py +0 -0
  37. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/agents/agent_web_retriever.py +0 -0
  38. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/agents/base_agent.py +0 -0
  39. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/agents/event_stream_handler.py +0 -0
  40. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/agents/json_agent.py +0 -0
  41. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/agents/retry_profiles.py +0 -0
  42. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/agents/run_stats.py +0 -0
  43. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/agents/smooth_stream.py +0 -0
  44. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/agents/subagent_stream_handler.py +0 -0
  45. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/atomic_io.py +0 -0
  46. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/atomic_json.py +0 -0
  47. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/chatgpt_codex_client.py +0 -0
  48. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/claude_cache_client.py +0 -0
  49. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/cli_runner.py +0 -0
  50. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/__init__.py +0 -0
  51. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/add_model_details.py +0 -0
  52. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/agent_menu.py +0 -0
  53. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/attachments.py +0 -0
  54. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/autosave_menu.py +0 -0
  55. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/autosave_search.py +0 -0
  56. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/clipboard.py +0 -0
  57. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/colors_menu.py +0 -0
  58. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/command_handler.py +0 -0
  59. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/command_registry.py +0 -0
  60. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/completers.py +0 -0
  61. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/completion_cache.py +0 -0
  62. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/config_apply.py +0 -0
  63. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/config_commands.py +0 -0
  64. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/core_commands.py +0 -0
  65. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/file_index.py +0 -0
  66. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/file_path_completion.py +0 -0
  67. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/help_catalog.py +0 -0
  68. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/help_overlay.py +0 -0
  69. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/image_utils.py +0 -0
  70. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/load_context_completion.py +0 -0
  71. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/mcp/__init__.py +0 -0
  72. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/mcp/base.py +0 -0
  73. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/mcp/catalog_server_installer.py +0 -0
  74. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/mcp/custom_server_form.py +0 -0
  75. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/mcp/custom_server_installer.py +0 -0
  76. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/mcp/edit_command.py +0 -0
  77. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/mcp/handler.py +0 -0
  78. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/mcp/help_command.py +0 -0
  79. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/mcp/install_command.py +0 -0
  80. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/mcp/install_menu.py +0 -0
  81. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/mcp/list_command.py +0 -0
  82. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/mcp/logs_command.py +0 -0
  83. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/mcp/mcp_servers_store.py +0 -0
  84. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/mcp/remove_command.py +0 -0
  85. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/mcp/restart_command.py +0 -0
  86. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/mcp/search_command.py +0 -0
  87. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/mcp/silence_warning_command.py +0 -0
  88. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/mcp/start_all_command.py +0 -0
  89. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/mcp/start_command.py +0 -0
  90. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/mcp/status_command.py +0 -0
  91. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/mcp/stop_all_command.py +0 -0
  92. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/mcp/stop_command.py +0 -0
  93. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/mcp/utils.py +0 -0
  94. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/mcp/wizard_utils.py +0 -0
  95. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/mcp_binding_menu.py +0 -0
  96. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/mcp_completion.py +0 -0
  97. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/menu_session.py +0 -0
  98. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/model_picker_completion.py +0 -0
  99. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/model_settings_defs.py +0 -0
  100. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/model_settings_menu.py +0 -0
  101. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/onboarding_slides.py +0 -0
  102. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/onboarding_wizard.py +0 -0
  103. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/pagination.py +0 -0
  104. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/pin_command_completion.py +0 -0
  105. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/session_browser.py +0 -0
  106. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/session_browser_data.py +0 -0
  107. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/session_commands.py +0 -0
  108. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/set_menu.py +0 -0
  109. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/set_menu_catalog.py +0 -0
  110. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/set_menu_schema.py +0 -0
  111. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/set_menu_settings.py +0 -0
  112. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/set_menu_shims.py +0 -0
  113. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/set_menu_values.py +0 -0
  114. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/shell_passthrough.py +0 -0
  115. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/skills_completion.py +0 -0
  116. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/tui_style.py +0 -0
  117. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/uc_menu.py +0 -0
  118. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/command_line/utils.py +0 -0
  119. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/config_file.py +0 -0
  120. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/error_logging.py +0 -0
  121. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/gemini_code_assist.py +0 -0
  122. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/gemini_model.py +0 -0
  123. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/hook_engine/README.md +0 -0
  124. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/hook_engine/__init__.py +0 -0
  125. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/hook_engine/aliases.py +0 -0
  126. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/hook_engine/engine.py +0 -0
  127. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/hook_engine/executor.py +0 -0
  128. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/hook_engine/matcher.py +0 -0
  129. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/hook_engine/models.py +0 -0
  130. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/hook_engine/registry.py +0 -0
  131. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/hook_engine/validator.py +0 -0
  132. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/http_utils.py +0 -0
  133. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/i18n/__init__.py +0 -0
  134. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/i18n/audit.py +0 -0
  135. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/i18n/catalog.py +0 -0
  136. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/i18n/formats.py +0 -0
  137. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/i18n/locale.py +0 -0
  138. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/i18n/locales/en-US.json +0 -0
  139. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/i18n/locales/es.json +0 -0
  140. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/i18n/locales/fr-CA.json +0 -0
  141. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/i18n/plurals.py +0 -0
  142. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/i18n/pseudo.py +0 -0
  143. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/i18n/translate.py +0 -0
  144. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/kennel_provider.py +0 -0
  145. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/keymap.py +0 -0
  146. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/list_filtering.py +0 -0
  147. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/main.py +0 -0
  148. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/mcp_/__init__.py +0 -0
  149. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/mcp_/agent_bindings.py +0 -0
  150. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/mcp_/async_lifecycle.py +0 -0
  151. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/mcp_/blocking_startup.py +0 -0
  152. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/mcp_/circuit_breaker.py +0 -0
  153. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/mcp_/config_wizard.py +0 -0
  154. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/mcp_/dashboard.py +0 -0
  155. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/mcp_/error_isolation.py +0 -0
  156. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/mcp_/examples/retry_example.py +0 -0
  157. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/mcp_/health_monitor.py +0 -0
  158. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/mcp_/managed_server.py +0 -0
  159. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/mcp_/mcp_logs.py +0 -0
  160. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/mcp_/project_config.py +0 -0
  161. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/mcp_/registry.py +0 -0
  162. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/mcp_/retry_manager.py +0 -0
  163. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/mcp_/server_registry_catalog.py +0 -0
  164. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/mcp_/status_tracker.py +0 -0
  165. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/mcp_/system_tools.py +0 -0
  166. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/mcp_/tool_arg_coercion.py +0 -0
  167. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/mcp_/toolset_utils.py +0 -0
  168. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/mcp_prompts/__init__.py +0 -0
  169. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/mcp_prompts/hook_creator.py +0 -0
  170. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/messaging/__init__.py +0 -0
  171. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/messaging/bar_painters.py +0 -0
  172. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/messaging/bar_rendering.py +0 -0
  173. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/messaging/bottom_bar.py +0 -0
  174. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/messaging/bus.py +0 -0
  175. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/messaging/chords.py +0 -0
  176. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/messaging/commands.py +0 -0
  177. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/messaging/editor_actions.py +0 -0
  178. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/messaging/editor_completion.py +0 -0
  179. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/messaging/editor_display.py +0 -0
  180. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/messaging/editor_history.py +0 -0
  181. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/messaging/editor_keys.py +0 -0
  182. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/messaging/editor_paste.py +0 -0
  183. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/messaging/external_editor.py +0 -0
  184. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/messaging/inline_bar.py +0 -0
  185. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/messaging/line_editor.py +0 -0
  186. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/messaging/markdown_patches.py +0 -0
  187. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/messaging/message_queue.py +0 -0
  188. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/messaging/messages.py +0 -0
  189. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/messaging/pause_controller.py +0 -0
  190. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/messaging/prompt_prefix_style.py +0 -0
  191. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/messaging/queue_console.py +0 -0
  192. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/messaging/renderers.py +0 -0
  193. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/messaging/rich_renderer.py +0 -0
  194. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/messaging/run_ui.py +0 -0
  195. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/messaging/run_ui_wiring.py +0 -0
  196. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/messaging/spinner/__init__.py +0 -0
  197. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/messaging/subagent_console.py +0 -0
  198. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/messaging/transcript_guard.py +0 -0
  199. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/model_descriptions.py +0 -0
  200. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/model_factory.py +0 -0
  201. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/model_switching.py +0 -0
  202. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/model_utils.py +0 -0
  203. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/models.json +0 -0
  204. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/models_dev_api.json +0 -0
  205. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/models_dev_parser.py +0 -0
  206. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/observability.py +0 -0
  207. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/platform_utils.py +0 -0
  208. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/plugins/config.py +0 -0
  209. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/plugins/oauth_pasteback.py +0 -0
  210. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/plugins/oauth_puppy_html.py +0 -0
  211. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/plugins/trust.py +0 -0
  212. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/plugins/trust_notice.py +0 -0
  213. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/provider_credentials.py +0 -0
  214. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/provider_identity.py +0 -0
  215. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/pydantic_patches.py +0 -0
  216. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/reopenable_async_client.py +0 -0
  217. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/round_robin_model.py +0 -0
  218. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/secret_store.py +0 -0
  219. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/secret_store_backends.py +0 -0
  220. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/session_format_migration.py +0 -0
  221. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/session_lifecycle.py +0 -0
  222. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/session_migration.py +0 -0
  223. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/session_storage.py +0 -0
  224. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/skill_provider.py +0 -0
  225. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/splash.py +0 -0
  226. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/status_display.py +0 -0
  227. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/terminal_utils.py +0 -0
  228. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/token_usage.py +0 -0
  229. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/__init__.py +0 -0
  230. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/agent_tools.py +0 -0
  231. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/ask_user_question/__init__.py +0 -0
  232. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/ask_user_question/constants.py +0 -0
  233. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/ask_user_question/demo_tui.py +0 -0
  234. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/ask_user_question/handler.py +0 -0
  235. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/ask_user_question/models.py +0 -0
  236. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/ask_user_question/registration.py +0 -0
  237. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/ask_user_question/renderers.py +0 -0
  238. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/ask_user_question/terminal_ui.py +0 -0
  239. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/ask_user_question/theme.py +0 -0
  240. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/ask_user_question/tui_loop.py +0 -0
  241. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/browser/__init__.py +0 -0
  242. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/browser/browser_control.py +0 -0
  243. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/browser/browser_interactions.py +0 -0
  244. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/browser/browser_locator_resolver.py +0 -0
  245. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/browser/browser_locators.py +0 -0
  246. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/browser/browser_manager.py +0 -0
  247. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/browser/browser_navigation.py +0 -0
  248. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/browser/browser_page_snapshot.py +0 -0
  249. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/browser/browser_screenshot.py +0 -0
  250. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/browser/browser_scripts.py +0 -0
  251. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/browser/browser_semantic_interactions.py +0 -0
  252. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/browser/browser_workflows.py +0 -0
  253. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/browser/tool_registry.py +0 -0
  254. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/command_runner.py +0 -0
  255. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/common.py +0 -0
  256. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/display.py +0 -0
  257. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/file_operations.py +0 -0
  258. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/file_permission_state.py +0 -0
  259. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/fs_access.py +0 -0
  260. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/image_tools.py +0 -0
  261. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/io_backends.py +0 -0
  262. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/model_tools.py +0 -0
  263. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/shell_backgrounding.py +0 -0
  264. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/skills_tools.py +0 -0
  265. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/subagent_context.py +0 -0
  266. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/subagent_invocation.py +0 -0
  267. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/subagent_usage_metrics.py +0 -0
  268. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/tools_content.py +0 -0
  269. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/tools/universal_constructor.py +0 -0
  270. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/undo_manager.py +0 -0
  271. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/universal_constructor_provider.py +0 -0
  272. {code_puppy-0.0.783 → code_puppy-0.0.785}/code_puppy/version_checker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: code-puppy
3
- Version: 0.0.783
3
+ Version: 0.0.785
4
4
  Summary: Code generation agent
5
5
  Project-URL: repository, https://github.com/mpfaffenberger/code_puppy
6
6
  Project-URL: HomePage, https://github.com/mpfaffenberger/code_puppy
@@ -20,7 +20,7 @@ Requires-Dist: anthropic<1,>=0.108.0
20
20
  Requires-Dist: anyio>=4.7.0
21
21
  Requires-Dist: azure-identity>=1.15.0
22
22
  Requires-Dist: boto3>=1.43.9
23
- Requires-Dist: code-puppy-core-plugins>=0.0.26
23
+ Requires-Dist: code-puppy-core-plugins>=0.0.27
24
24
  Requires-Dist: httpx[http2]>=0.24.1
25
25
  Requires-Dist: json-repair>=0.46.2
26
26
  Requires-Dist: keyring>=24.0.0
@@ -49,6 +49,7 @@ PhaseType = Literal[
49
49
  "agent_run_end",
50
50
  "agent_run_result",
51
51
  "model_select",
52
+ "provider_credential_flow",
52
53
  "register_mcp_catalog_servers",
53
54
  "register_browser_types",
54
55
  "register_model_providers",
@@ -138,6 +139,7 @@ _callbacks: Dict[PhaseType, List[CallbackFunc]] = {
138
139
  "agent_run_end": [],
139
140
  "agent_run_result": [],
140
141
  "model_select": [],
142
+ "provider_credential_flow": [],
141
143
  "register_mcp_catalog_servers": [],
142
144
  "register_browser_types": [],
143
145
  "register_model_providers": [],
@@ -1225,6 +1227,44 @@ def on_model_select(
1225
1227
  return None
1226
1228
 
1227
1229
 
1230
+ def on_provider_credential_flow(*, provider_id: str, env_var: str) -> bool:
1231
+ """Ask plugins to acquire a missing provider credential.
1232
+
1233
+ Fired by the ``/add_model`` flow for each required env var that is not
1234
+ set, before falling back to manual key entry. A plugin that can mint the
1235
+ credential itself (e.g. an OAuth flow that exchanges an authorization
1236
+ code for an API key) should persist it so it takes effect immediately
1237
+ (config + ``os.environ``) and return ``True``. Return ``None``/``False``
1238
+ to defer to manual entry.
1239
+
1240
+ Unlike most hooks this one deliberately short-circuits: callbacks run
1241
+ interactive, side-effectful flows, so the first one to return ``True``
1242
+ wins and the rest are never invoked. Callbacks must gate on
1243
+ ``provider_id``/``env_var`` and return ``None`` immediately for
1244
+ providers they do not own.
1245
+
1246
+ Args:
1247
+ provider_id: models.dev provider id (e.g. ``"openrouter"``).
1248
+ env_var: The credential env var being requested.
1249
+
1250
+ Returns:
1251
+ True when a callback reports the credential as acquired.
1252
+ """
1253
+ for callback in get_callbacks("provider_credential_flow"):
1254
+ try:
1255
+ result = callback(provider_id=provider_id, env_var=env_var)
1256
+ except Exception as e:
1257
+ logger.error(
1258
+ f"Callback {getattr(callback, '__name__', callback)!r} failed "
1259
+ f"in phase 'provider_credential_flow': {e}\n"
1260
+ f"{traceback.format_exc()}"
1261
+ )
1262
+ continue
1263
+ if result is True:
1264
+ return True
1265
+ return False
1266
+
1267
+
1228
1268
  async def on_agent_run_end(
1229
1269
  agent_name: str,
1230
1270
  model_name: str,
@@ -346,16 +346,34 @@ def _text_input(title: str, **kwargs):
346
346
  return builder
347
347
 
348
348
 
349
+ def _plugin_credential_flow(provider: ProviderInfo, env_var: str) -> bool:
350
+ """Give plugins first crack at acquiring a missing credential.
351
+
352
+ A plugin (e.g. an OAuth flow that exchanges a code for an API key) saves
353
+ the credential itself and returns True via the ``provider_credential_flow``
354
+ hook. Trust but verify: the env var must actually be set afterwards,
355
+ otherwise we fall back to manual entry anyway.
356
+ """
357
+ from code_puppy.callbacks import on_provider_credential_flow
358
+
359
+ handled = on_provider_credential_flow(provider_id=provider.id, env_var=env_var)
360
+ return bool(handled and os.environ.get(env_var))
361
+
362
+
349
363
  def prompt_for_credentials(provider: ProviderInfo, **overrides) -> bool:
350
364
  """Prompt for each missing credential. Empty skips; Esc cancels all.
351
365
 
352
- Saves via config + os.environ so the key is immediately usable.
366
+ Plugins get first crack via the ``provider_credential_flow`` hook (e.g.
367
+ browser OAuth); anything unhandled falls back to manual entry, which
368
+ saves via config + os.environ so the key is immediately usable.
353
369
  """
354
370
  missing = missing_env_vars(provider)
355
371
  if not missing:
356
372
  emit_info(t("model_menu.credentials.all_set", provider=provider.name))
357
373
  return True
358
374
  for env_var in missing:
375
+ if _plugin_credential_flow(provider, env_var):
376
+ continue
359
377
  result = (
360
378
  _text_input(
361
379
  f"{provider.name}: {env_var}",
@@ -150,11 +150,14 @@ class TrustCommand(MCPCommandBase):
150
150
 
151
151
  def _revoke(self, group_id: str) -> None:
152
152
  if revoke_project_mcp():
153
+ try:
154
+ self.manager.sync_from_config()
155
+ except Exception as exc: # pragma: no cover - defensive
156
+ logger.warning("Post-revoke registry sync failed: %s", exc)
153
157
  emit_info(
154
158
  Text.from_markup(
155
159
  "[green]\u2713 Revoked[/green] trust for this project's MCP "
156
- "config. Its servers will no longer load. Restart or re-sync "
157
- "to drop already-registered ones."
160
+ "config. Its servers will no longer load."
158
161
  ),
159
162
  message_group=group_id,
160
163
  )
@@ -595,7 +595,7 @@ def _parse_mcp_servers_mapping(raw_text: str) -> dict:
595
595
  return servers
596
596
 
597
597
 
598
- def load_mcp_server_configs():
598
+ def load_mcp_server_configs(*, raise_on_error: bool = False):
599
599
  """Load MCP server configs, merging user-level and trusted project-level.
600
600
 
601
601
  Sources, in ascending order of precedence:
@@ -610,6 +610,11 @@ def load_mcp_server_configs():
610
610
  Project entries win on name collision, matching how project agents, skills,
611
611
  and plugins override their user-level counterparts. Returns an empty dict
612
612
  when nothing is configured.
613
+
614
+ When *raise_on_error* is true, a parse/IO failure of an existing user-level
615
+ file (or a failure of the project loader) is re-raised after the error is
616
+ emitted, so callers that unregister missing names can skip that drop
617
+ instead of treating ``{}`` as "configure nothing".
613
618
  """
614
619
  from code_puppy.messaging.message_queue import emit_error
615
620
 
@@ -622,6 +627,8 @@ def load_mcp_server_configs():
622
627
  configs.update(_parse_mcp_servers_mapping(f.read()))
623
628
  except Exception as e:
624
629
  emit_error(f"Failed to load MCP servers - {str(e)}")
630
+ if raise_on_error:
631
+ raise
625
632
 
626
633
  # 2. Project-level config (opt-in, trust-gated). A broken or untrusted
627
634
  # project file must never break user-level loading.
@@ -633,6 +640,8 @@ def load_mcp_server_configs():
633
640
  configs.update(project_configs)
634
641
  except Exception as e:
635
642
  emit_error(f"Failed to load project MCP servers - {str(e)}")
643
+ if raise_on_error:
644
+ raise
636
645
 
637
646
  return configs
638
647
 
@@ -161,10 +161,7 @@ class MCPManager:
161
161
  try:
162
162
  from code_puppy.config import load_mcp_server_configs
163
163
 
164
- configs = load_mcp_server_configs()
165
- if not configs:
166
- logger.debug("No servers found in mcp_servers.json")
167
- return
164
+ configs = load_mcp_server_configs(raise_on_error=True) or {}
168
165
 
169
166
  synced_count = 0
170
167
  updated_count = 0
@@ -209,9 +206,23 @@ class MCPManager:
209
206
  logger.warning(f"Failed to sync server '{name}' from config: {e}")
210
207
  continue
211
208
 
212
- if synced_count > 0 or updated_count > 0:
209
+ configured_names = {
210
+ name for name, conf in configs.items() if isinstance(conf, dict)
211
+ }
212
+ dropped_count = 0
213
+ for existing in list(self.registry.list_all()):
214
+ if existing.name not in configured_names:
215
+ if self.remove_server(existing.id):
216
+ dropped_count += 1
217
+ logger.debug(
218
+ "Dropped server no longer in mcp_servers.json: %s",
219
+ existing.name,
220
+ )
221
+
222
+ if synced_count > 0 or updated_count > 0 or dropped_count > 0:
213
223
  logger.info(
214
- f"Synced {synced_count} new and updated {updated_count} servers from mcp_servers.json"
224
+ f"Synced {synced_count} new, updated {updated_count}, "
225
+ f"dropped {dropped_count} servers from mcp_servers.json"
215
226
  )
216
227
 
217
228
  except Exception as e:
@@ -117,6 +117,18 @@ def _install_project_plugin_finder() -> None:
117
117
 
118
118
  PLUGIN_ENTRY_POINT_GROUP = "code_puppy.plugins"
119
119
 
120
+ # shell_safety implements the safety_permission_level threshold. Skip it only
121
+ # when the user opted into high/critical autonomy; the default (medium) must
122
+ # load it or the setting is a no-op.
123
+ _SHELL_SAFETY_SKIP_LEVELS = frozenset({"high", "critical"})
124
+
125
+
126
+ def _skip_shell_safety_plugin() -> bool:
127
+ from code_puppy.config import get_safety_permission_level
128
+
129
+ return get_safety_permission_level() in _SHELL_SAFETY_SKIP_LEVELS
130
+
131
+
120
132
  # Track if plugins have already been loaded to prevent duplicate registration
121
133
  _PLUGINS_LOADED = False
122
134
 
@@ -135,18 +147,13 @@ def _load_installed_plugins() -> list[str]:
135
147
  project plugins, but remain physically independent from the core package.
136
148
  Entry points are sorted for deterministic startup and test behavior.
137
149
  """
138
- from code_puppy.config import get_safety_permission_level
139
-
140
150
  loaded: list[str] = []
141
151
  discovered = sorted(
142
152
  entry_points(group=PLUGIN_ENTRY_POINT_GROUP), key=lambda item: item.name
143
153
  )
144
154
  for entry_point in discovered:
145
155
  plugin_name = entry_point.name
146
- if plugin_name == "shell_safety" and get_safety_permission_level() not in (
147
- "none",
148
- "low",
149
- ):
156
+ if plugin_name == "shell_safety" and _skip_shell_safety_plugin():
150
157
  logger.debug("Skipping shell_safety plugin due to safety permission level")
151
158
  continue
152
159
  try:
@@ -191,14 +198,12 @@ def _load_builtin_plugins(
191
198
  continue
192
199
 
193
200
  if callbacks_file.exists():
194
- # Skip shell_safety plugin unless safety_permission_level is "low" or "none"
195
- if plugin_name == "shell_safety":
196
- safety_level = get_safety_permission_level()
197
- if safety_level not in ("none", "low"):
198
- logger.debug(
199
- f"Skipping shell_safety plugin - safety_permission_level is '{safety_level}' (needs 'low' or 'none')"
200
- )
201
- continue
201
+ if plugin_name == "shell_safety" and _skip_shell_safety_plugin():
202
+ logger.debug(
203
+ "Skipping shell_safety plugin - safety_permission_level is %s",
204
+ get_safety_permission_level(),
205
+ )
206
+ continue
202
207
 
203
208
  try:
204
209
  module_name = f"code_puppy.plugins.{plugin_name}.register_callbacks"
@@ -72,9 +72,9 @@ _TZINFO_EQUIVALENTS: Dict[Tuple[str, str], Callable[..., Any]] = {
72
72
  ("pydantic_core", "TzInfo"): _tz_from_utc_offset,
73
73
  }
74
74
 
75
- # Genuine timezone libraries: unpickle their classes for real when the
76
- # library is installed; otherwise fall back to surrogates as usual.
77
- _TZ_LIBRARY_PREFIXES = ("pytz", "dateutil.tz")
75
+ # pytz / dateutil.tz must NOT go through super().find_class: those packages
76
+ # re-export os/sys the same way uuid and collections did. Known tzinfo
77
+ # shapes are rebuilt via _TZINFO_EQUIVALENTS; everything else is a surrogate.
78
78
 
79
79
 
80
80
  class SurrogateBase:
@@ -145,14 +145,6 @@ class SurrogateUnpickler(pickle.Unpickler):
145
145
  equivalent = _TZINFO_EQUIVALENTS.get((module, name))
146
146
  if equivalent is not None:
147
147
  return equivalent
148
- if any(
149
- module == prefix or module.startswith(prefix + ".")
150
- for prefix in _TZ_LIBRARY_PREFIXES
151
- ):
152
- try:
153
- return super().find_class(module, name)
154
- except Exception: # noqa: BLE001 - library absent/renamed
155
- pass
156
148
  return self._surrogate_for(module, name)
157
149
 
158
150
  def _tz_tolerant(self, obj: Any) -> Any:
@@ -14,6 +14,7 @@ import difflib
14
14
  import json
15
15
  import os
16
16
  import traceback
17
+ from pathlib import Path
17
18
  from code_puppy.undo_manager import UndoManager
18
19
  import warnings
19
20
  from typing import Annotated, Any, Dict, List, Union
@@ -45,6 +46,75 @@ from code_puppy.tools.file_permission_state import (
45
46
  )
46
47
 
47
48
 
49
+ def _split_existing(path: Path) -> tuple[Path, tuple[str, ...]]:
50
+ """Deepest existing ancestor of *path*, plus the missing trailing names."""
51
+ missing: list[str] = []
52
+ current = path
53
+ while True:
54
+ if current.exists():
55
+ return current.resolve(), tuple(reversed(missing))
56
+ parent = current.parent
57
+ if parent == current:
58
+ return current, tuple(reversed(missing))
59
+ missing.append(current.name)
60
+ current = parent
61
+
62
+
63
+ def _casefold_has_prefix(parts: tuple[str, ...], prefix: tuple[str, ...]) -> bool:
64
+ if len(parts) < len(prefix):
65
+ return False
66
+ return all(a.casefold() == b.casefold() for a, b in zip(prefix, parts))
67
+
68
+
69
+ def _is_inside_user_plugin_root(target: Path, root: Path) -> bool:
70
+ """Containment that survives APFS case-folding. ``Path.resolve`` does not."""
71
+ target_existing, target_rest = _split_existing(target)
72
+ root_existing, root_rest = _split_existing(root)
73
+
74
+ if os.path.samefile(target_existing, root_existing):
75
+ return _casefold_has_prefix(target_rest, root_rest)
76
+
77
+ current = target_existing
78
+ while True:
79
+ if os.path.samefile(current, root_existing):
80
+ return not root_rest
81
+ parent = current.parent
82
+ if parent == current:
83
+ return False
84
+ current = parent
85
+
86
+
87
+ def _is_user_plugin_tree_path(file_path: str) -> bool:
88
+ """True if *file_path* is inside ``~/.code_puppy/plugins``.
89
+
90
+ That tree is imported at the next process start with no trust ceremony.
91
+ File tools must not plant ``register_callbacks.py`` there.
92
+ Canonicalization errors fail closed (treated as inside).
93
+ """
94
+ from code_puppy.plugins import USER_PLUGINS_DIR
95
+
96
+ try:
97
+ resolved = Path(resolve_path(file_path)).resolve()
98
+ root = Path(USER_PLUGINS_DIR).expanduser().resolve()
99
+ return _is_inside_user_plugin_root(resolved, root)
100
+ except (OSError, RuntimeError, ValueError):
101
+ return True
102
+
103
+
104
+ def _refuse_user_plugin_tree(file_path: str) -> Dict[str, Any] | None:
105
+ if not _is_user_plugin_tree_path(file_path):
106
+ return None
107
+ return {
108
+ "success": False,
109
+ "path": file_path,
110
+ "message": (
111
+ "Refused: file tools cannot modify ~/.code_puppy/plugins. "
112
+ "That directory is imported at startup."
113
+ ),
114
+ "changed": False,
115
+ }
116
+
117
+
48
118
  def _permission_denied(permission_results: List[Any]) -> bool:
49
119
  """Return True when any permission callback explicitly denies.
50
120
 
@@ -424,6 +494,9 @@ def _write_to_file(
424
494
  def delete_snippet_from_file(
425
495
  context: RunContext, file_path: str, snippet: str, message_group: str | None = None
426
496
  ) -> Dict[str, Any]:
497
+ refused = _refuse_user_plugin_tree(file_path)
498
+ if refused is not None:
499
+ return refused
427
500
  # Use the plugin system for permission handling with operation data
428
501
  from code_puppy.callbacks import on_file_permission
429
502
 
@@ -452,6 +525,9 @@ def write_to_file(
452
525
  overwrite: bool,
453
526
  message_group: str | None = None,
454
527
  ) -> Dict[str, Any]:
528
+ refused = _refuse_user_plugin_tree(path)
529
+ if refused is not None:
530
+ return refused
455
531
  # Use the plugin system for permission handling with operation data
456
532
  from code_puppy.callbacks import on_file_permission
457
533
 
@@ -481,6 +557,9 @@ def replace_in_file(
481
557
  replacements: List[Dict[str, str]],
482
558
  message_group: str | None = None,
483
559
  ) -> Dict[str, Any]:
560
+ refused = _refuse_user_plugin_tree(path)
561
+ if refused is not None:
562
+ return refused
484
563
  # Use the plugin system for permission handling with operation data
485
564
  from code_puppy.callbacks import on_file_permission
486
565
 
@@ -504,6 +583,9 @@ async def delete_snippet_from_file_async(
504
583
  context: RunContext, file_path: str, snippet: str, message_group: str | None = None
505
584
  ) -> Dict[str, Any]:
506
585
  """Async permission-aware variant of ``delete_snippet_from_file``."""
586
+ refused = _refuse_user_plugin_tree(file_path)
587
+ if refused is not None:
588
+ return refused
507
589
  from code_puppy.callbacks import on_file_permission_async
508
590
 
509
591
  operation_data = {"snippet": snippet}
@@ -530,6 +612,9 @@ async def write_to_file_async(
530
612
  message_group: str | None = None,
531
613
  ) -> Dict[str, Any]:
532
614
  """Async permission-aware variant of ``write_to_file``."""
615
+ refused = _refuse_user_plugin_tree(path)
616
+ if refused is not None:
617
+ return refused
533
618
  from code_puppy.callbacks import on_file_permission_async
534
619
 
535
620
  operation_data = {"content": content, "overwrite": overwrite}
@@ -556,6 +641,9 @@ async def replace_in_file_async(
556
641
  message_group: str | None = None,
557
642
  ) -> Dict[str, Any]:
558
643
  """Async permission-aware variant of ``replace_in_file``."""
644
+ refused = _refuse_user_plugin_tree(path)
645
+ if refused is not None:
646
+ return refused
559
647
  from code_puppy.callbacks import on_file_permission_async
560
648
 
561
649
  operation_data = {"replacements": replacements}
@@ -726,6 +814,9 @@ async def _edit_file_async(
726
814
  def _delete_file(
727
815
  context: RunContext, file_path: str, message_group: str | None = None
728
816
  ) -> Dict[str, Any]:
817
+ refused = _refuse_user_plugin_tree(file_path)
818
+ if refused is not None:
819
+ return refused
729
820
  UndoManager().record_change(file_path, "delete_file")
730
821
  file_path = resolve_path(file_path)
731
822
 
@@ -786,6 +877,9 @@ async def _delete_file_async(
786
877
  context: RunContext, file_path: str, message_group: str | None = None
787
878
  ) -> Dict[str, Any]:
788
879
  """Async permission-aware variant of ``_delete_file``."""
880
+ refused = _refuse_user_plugin_tree(file_path)
881
+ if refused is not None:
882
+ return refused
789
883
  file_path = resolve_path(file_path)
790
884
 
791
885
  from code_puppy.callbacks import on_file_permission_async
@@ -4,12 +4,12 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "code-puppy"
7
- version = "0.0.783"
7
+ version = "0.0.785"
8
8
  description = "Code generation agent"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11,<3.15"
11
11
  dependencies = [
12
- "code-puppy-core-plugins>=0.0.26",
12
+ "code-puppy-core-plugins>=0.0.27",
13
13
  "logfire>=4.0",
14
14
  "pydantic-ai-slim[openai,anthropic,mcp]==2.31.0",
15
15
  "pydantic-ai-harness==0.23.0",
File without changes
File without changes
File without changes