meshcode 2.11.8__tar.gz → 2.11.10__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 (219) hide show
  1. {meshcode-2.11.8 → meshcode-2.11.10}/PKG-INFO +1 -1
  2. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/__init__.py +1 -1
  3. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/comms_v4.py +58 -4
  4. meshcode-2.11.10/meshcode/doctor.py +277 -0
  5. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/meshcode_mcp/server.py +80 -32
  6. meshcode-2.11.10/meshcode/meshcode_mcp/test_install_guard.py +75 -0
  7. meshcode-2.11.10/meshcode/meshcode_mcp/test_prefs_claude_version.py +82 -0
  8. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/run_agent.py +62 -8
  9. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode.egg-info/PKG-INFO +1 -1
  10. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode.egg-info/SOURCES.txt +4 -0
  11. {meshcode-2.11.8 → meshcode-2.11.10}/pyproject.toml +1 -1
  12. meshcode-2.11.10/tests/test_doctor.py +123 -0
  13. {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_install_guard.py +68 -10
  14. {meshcode-2.11.8 → meshcode-2.11.10}/README.md +0 -0
  15. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/_stop_hook_template.py +0 -0
  16. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/ascii_art.py +0 -0
  17. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/cli.py +0 -0
  18. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/compat.py +0 -0
  19. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/daemon.py +0 -0
  20. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/error_hints.py +0 -0
  21. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/exceptions.py +0 -0
  22. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/invites.py +0 -0
  23. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/launcher.py +0 -0
  24. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/launcher_install.py +0 -0
  25. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/meshcode_mcp/__init__.py +0 -0
  26. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/meshcode_mcp/__main__.py +0 -0
  27. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/meshcode_mcp/backend.py +0 -0
  28. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/meshcode_mcp/realtime.py +0 -0
  29. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
  30. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/meshcode_mcp/test_backend.py +0 -0
  31. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
  32. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  33. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  34. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/preferences.py +0 -0
  35. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/protocol_handler.py +0 -0
  36. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/protocol_v2.py +0 -0
  37. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/quickstart.py +0 -0
  38. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/scripts/check_secrets.py +0 -0
  39. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/scripts/race_rate_harness.py +0 -0
  40. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/secrets.py +0 -0
  41. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/self_update.py +0 -0
  42. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/setup_clients.py +0 -0
  43. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/supervisor.py +0 -0
  44. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/upload.py +0 -0
  45. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/comms_v4.py +0 -0
  46. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/__init__.py +0 -0
  47. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/ascii_art.py +0 -0
  48. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/cli.py +0 -0
  49. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/comms_v4.py +0 -0
  50. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/compat.py +0 -0
  51. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/error_hints.py +0 -0
  52. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/exceptions.py +0 -0
  53. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/invites.py +0 -0
  54. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/launcher.py +0 -0
  55. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/launcher_install.py +0 -0
  56. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/meshcode_mcp/__init__.py +0 -0
  57. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/meshcode_mcp/__main__.py +0 -0
  58. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/meshcode_mcp/backend.py +0 -0
  59. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/meshcode_mcp/realtime.py +0 -0
  60. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/meshcode_mcp/server.py +0 -0
  61. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/meshcode_mcp/test_backend.py +0 -0
  62. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/meshcode_mcp/test_realtime.py +0 -0
  63. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  64. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/preferences.py +0 -0
  65. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/protocol_v2.py +0 -0
  66. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/quickstart.py +0 -0
  67. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/run_agent.py +0 -0
  68. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/secrets.py +0 -0
  69. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/self_update.py +0 -0
  70. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/setup_clients.py +0 -0
  71. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/supervisor.py +0 -0
  72. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/upload.py +0 -0
  73. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/scripts/sentinel.py +0 -0
  74. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/tests/test_core.py +0 -0
  75. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/tests/test_cross_agent_messaging.py +0 -0
  76. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/tests/test_esc_deaf_state.py +0 -0
  77. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/tests/test_exceptions.py +0 -0
  78. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/tests/test_mark_read_batch.py +0 -0
  79. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/tests/test_migration_integrity.py +0 -0
  80. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/tests/test_realtime_event_freshness.py +0 -0
  81. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/tests/test_rls_cross_tenant.py +0 -0
  82. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/tests/test_rpc_migrations.py +0 -0
  83. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/tests/test_security_regressions.py +0 -0
  84. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/tests/test_sentinel.py +0 -0
  85. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/tests/test_status_enum_coverage.py +0 -0
  86. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/__init__.py +0 -0
  87. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/ascii_art.py +0 -0
  88. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/cli.py +0 -0
  89. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/comms_v4.py +0 -0
  90. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/compat.py +0 -0
  91. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/error_hints.py +0 -0
  92. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/exceptions.py +0 -0
  93. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/invites.py +0 -0
  94. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/launcher.py +0 -0
  95. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/launcher_install.py +0 -0
  96. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/__init__.py +0 -0
  97. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/__main__.py +0 -0
  98. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/backend.py +0 -0
  99. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/realtime.py +0 -0
  100. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/server.py +0 -0
  101. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/test_backend.py +0 -0
  102. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/test_realtime.py +0 -0
  103. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  104. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/preferences.py +0 -0
  105. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/protocol_v2.py +0 -0
  106. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/quickstart.py +0 -0
  107. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/run_agent.py +0 -0
  108. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/secrets.py +0 -0
  109. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/self_update.py +0 -0
  110. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/setup_clients.py +0 -0
  111. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/supervisor.py +0 -0
  112. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/upload.py +0 -0
  113. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/comms_v4.py +0 -0
  114. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/__init__.py +0 -0
  115. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/ascii_art.py +0 -0
  116. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/cli.py +0 -0
  117. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/comms_v4.py +0 -0
  118. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/compat.py +0 -0
  119. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/error_hints.py +0 -0
  120. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/exceptions.py +0 -0
  121. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/invites.py +0 -0
  122. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/launcher.py +0 -0
  123. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/launcher_install.py +0 -0
  124. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/meshcode_mcp/__init__.py +0 -0
  125. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/meshcode_mcp/__main__.py +0 -0
  126. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/meshcode_mcp/backend.py +0 -0
  127. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/meshcode_mcp/realtime.py +0 -0
  128. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/meshcode_mcp/server.py +0 -0
  129. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/meshcode_mcp/test_backend.py +0 -0
  130. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/meshcode_mcp/test_realtime.py +0 -0
  131. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  132. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/preferences.py +0 -0
  133. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/protocol_v2.py +0 -0
  134. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/quickstart.py +0 -0
  135. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/run_agent.py +0 -0
  136. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/secrets.py +0 -0
  137. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/self_update.py +0 -0
  138. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/setup_clients.py +0 -0
  139. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/supervisor.py +0 -0
  140. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/upload.py +0 -0
  141. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/scripts/sentinel.py +0 -0
  142. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/tests/test_core.py +0 -0
  143. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/tests/test_cross_agent_messaging.py +0 -0
  144. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/tests/test_esc_deaf_state.py +0 -0
  145. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/tests/test_exceptions.py +0 -0
  146. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/tests/test_mark_read_batch.py +0 -0
  147. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/tests/test_migration_integrity.py +0 -0
  148. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/tests/test_realtime_event_freshness.py +0 -0
  149. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/tests/test_rls_cross_tenant.py +0 -0
  150. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/tests/test_rpc_migrations.py +0 -0
  151. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/tests/test_security_regressions.py +0 -0
  152. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/tests/test_sentinel.py +0 -0
  153. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/tests/test_status_enum_coverage.py +0 -0
  154. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/comms_v4.py +0 -0
  155. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/__init__.py +0 -0
  156. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/ascii_art.py +0 -0
  157. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/cli.py +0 -0
  158. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/comms_v4.py +0 -0
  159. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/compat.py +0 -0
  160. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/error_hints.py +0 -0
  161. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/exceptions.py +0 -0
  162. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/invites.py +0 -0
  163. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/launcher.py +0 -0
  164. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/launcher_install.py +0 -0
  165. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/meshcode_mcp/__init__.py +0 -0
  166. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/meshcode_mcp/__main__.py +0 -0
  167. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/meshcode_mcp/backend.py +0 -0
  168. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/meshcode_mcp/realtime.py +0 -0
  169. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/meshcode_mcp/server.py +0 -0
  170. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/meshcode_mcp/test_backend.py +0 -0
  171. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/meshcode_mcp/test_realtime.py +0 -0
  172. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  173. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/preferences.py +0 -0
  174. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/protocol_v2.py +0 -0
  175. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/quickstart.py +0 -0
  176. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/run_agent.py +0 -0
  177. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/secrets.py +0 -0
  178. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/self_update.py +0 -0
  179. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/setup_clients.py +0 -0
  180. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/supervisor.py +0 -0
  181. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/upload.py +0 -0
  182. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/scripts/sentinel.py +0 -0
  183. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/tests/test_core.py +0 -0
  184. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/tests/test_cross_agent_messaging.py +0 -0
  185. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/tests/test_esc_deaf_state.py +0 -0
  186. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/tests/test_exceptions.py +0 -0
  187. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/tests/test_mark_read_batch.py +0 -0
  188. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/tests/test_migration_integrity.py +0 -0
  189. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/tests/test_realtime_event_freshness.py +0 -0
  190. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/tests/test_rls_cross_tenant.py +0 -0
  191. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/tests/test_rpc_migrations.py +0 -0
  192. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/tests/test_security_regressions.py +0 -0
  193. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/tests/test_sentinel.py +0 -0
  194. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/tests/test_status_enum_coverage.py +0 -0
  195. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode.egg-info/dependency_links.txt +0 -0
  196. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode.egg-info/entry_points.txt +0 -0
  197. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode.egg-info/requires.txt +0 -0
  198. {meshcode-2.11.8 → meshcode-2.11.10}/meshcode.egg-info/top_level.txt +0 -0
  199. {meshcode-2.11.8 → meshcode-2.11.10}/setup.cfg +0 -0
  200. {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_auto_update_hardening.py +0 -0
  201. {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_boot_bug_regression.py +0 -0
  202. {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_core.py +0 -0
  203. {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_cross_agent_messaging.py +0 -0
  204. {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_epistemic_v1_python_sdk.py +0 -0
  205. {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_epistemic_v1_stop_conditions.py +0 -0
  206. {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_esc_deaf_state.py +0 -0
  207. {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_exceptions.py +0 -0
  208. {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_lease_sigterm_release.py +0 -0
  209. {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_mark_read_batch.py +0 -0
  210. {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_migration_integrity.py +0 -0
  211. {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_realtime_event_freshness.py +0 -0
  212. {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_rls_cross_tenant.py +0 -0
  213. {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_rpc_migrations.py +0 -0
  214. {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_security_regressions.py +0 -0
  215. {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_sentinel.py +0 -0
  216. {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_sleep_signals.py +0 -0
  217. {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_status_enum_coverage.py +0 -0
  218. {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_stay_on_loop_hook.py +0 -0
  219. {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_wait_open_tasks_contradiction.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.11.8
3
+ Version: 2.11.10
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -1,5 +1,5 @@
1
1
  """MeshCode — Real-time communication between AI agents."""
2
- __version__ = "2.11.8"
2
+ __version__ = "2.11.10"
3
3
 
4
4
  # Exception hierarchy — eagerly imported (lightweight, no deps)
5
5
  from meshcode.exceptions import ( # noqa: F401
@@ -2143,6 +2143,30 @@ def cmd_doctor(flags, pos):
2143
2143
  except Exception as e:
2144
2144
  fail("MCP server", f"import error: {e}. Run: pip install --upgrade meshcode")
2145
2145
 
2146
+ # 4d. Env checks (B2 + B9): Python stub, SSL, PATH, Claude, Node, git, prefs
2147
+ try:
2148
+ from meshcode.doctor import CHECKS as _env_checks
2149
+ # Skip ones cmd_doctor already covered (python version, api key)
2150
+ from meshcode.doctor import (
2151
+ _check_python,
2152
+ _check_api_key,
2153
+ _check_meshcode_on_path, # cmd_doctor doesn't check this
2154
+ )
2155
+ _seen = set()
2156
+ for _fn in _env_checks:
2157
+ if _fn in (_check_python, _check_api_key):
2158
+ continue
2159
+ try:
2160
+ _lvl, _title, _detail = _fn()
2161
+ if _title in _seen:
2162
+ continue
2163
+ _seen.add(_title)
2164
+ {"ok": ok, "warn": warn, "fail": fail}[_lvl](_title, _detail)
2165
+ except Exception as _e:
2166
+ warn(_fn.__name__.lstrip("_"), f"check crashed: {_e}")
2167
+ except ImportError:
2168
+ pass # doctor module not available (legacy install)
2169
+
2146
2170
  # 5. Stale agents check (requires auth)
2147
2171
  if api_key:
2148
2172
  try:
@@ -2745,8 +2769,33 @@ if __name__ == "__main__":
2745
2769
  print(" " + "=" * 40)
2746
2770
  print()
2747
2771
 
2748
- # Try device-code pairing first
2749
- pairing = sb_rpc("mc_create_pairing_session", {})
2772
+ # B2 pre-flight: refuse to onboard if env is broken
2773
+ from meshcode.doctor import run_doctor
2774
+ if run_doctor(skip_api_key_check=True) != 0:
2775
+ print()
2776
+ print(" [meshcode] Fix the blocker(s) above, then run `meshcode init` again.")
2777
+ sys.exit(1)
2778
+ print()
2779
+
2780
+ # B3: wrap pairing RPC so SSL / network errors abort cleanly (no
2781
+ # silent fall-through to API-key paste — friction #7).
2782
+ try:
2783
+ pairing = sb_rpc("mc_create_pairing_session", {})
2784
+ except Exception as _init_e:
2785
+ _msg = str(_init_e).lower()
2786
+ if "certificate" in _msg or "ssl" in _msg:
2787
+ print()
2788
+ print(" [meshcode] SSL certificate verification failed.")
2789
+ print(" [meshcode] This usually means your Python install is missing root certs.")
2790
+ _v = sys.version_info
2791
+ print(f" [meshcode] Fix: run /Applications/Python\\ {_v.major}.{_v.minor}/Install\\ Certificates.command")
2792
+ print(f" [meshcode] (Replace {_v.major}.{_v.minor} with your actual installed version.)")
2793
+ print(f" [meshcode] Alternative: pip install --upgrade certifi")
2794
+ else:
2795
+ print()
2796
+ print(f" [meshcode] Pairing service unreachable: {_init_e}")
2797
+ print(f" [meshcode] Check your network, then re-run `meshcode init`.")
2798
+ sys.exit(1)
2750
2799
  if pairing and isinstance(pairing, dict) and pairing.get("ok"):
2751
2800
  session_id = pairing["session_id"]
2752
2801
  code = pairing["code"]
@@ -3397,11 +3446,16 @@ if __name__ == "__main__":
3397
3446
  from meshcode.preferences import load_prefs, save_prefs
3398
3447
  if len(sys.argv) > 3:
3399
3448
  ver = sys.argv[3].strip()
3400
- if ver in ("reset", "unpin", "latest"):
3449
+ if ver in ("reset", "unpin"):
3401
3450
  prefs = load_prefs()
3402
3451
  prefs.pop("claude_version", None)
3403
3452
  save_prefs(prefs)
3404
- print("[meshcode] Claude Code version pin removed — will use installed version")
3453
+ print("[meshcode] Claude Code version pin removed — will use default ESC-safe pin")
3454
+ elif ver in ("latest", "none", "skip"):
3455
+ prefs = load_prefs()
3456
+ prefs["claude_version"] = ver
3457
+ save_prefs(prefs)
3458
+ print(f"[meshcode] Claude Code version pinning disabled ({ver}) — using installed version")
3405
3459
  else:
3406
3460
  prefs = load_prefs()
3407
3461
  prefs["claude_version"] = ver
@@ -0,0 +1,277 @@
1
+ """Pre-flight environment checks. `meshcode doctor` runs all and reports.
2
+
3
+ Each check returns (level, title, detail):
4
+ level: "ok" | "warn" | "fail"
5
+ title: short identifier shown to user
6
+ detail: actionable fix when not OK ("" when OK)
7
+
8
+ Exit code: 1 if any "fail", else 0 (warnings allowed).
9
+
10
+ Covers frictions #4 Python stub, #5 SSL certs, #8 PATH, #10 git/CLT modal,
11
+ #11 Claude not installed. Used as pre-flight by `meshcode init`.
12
+ """
13
+ import os
14
+ import sys
15
+ import json
16
+ import shutil
17
+ import subprocess
18
+ import urllib.request
19
+ import urllib.error
20
+ from typing import List, Tuple
21
+
22
+ Status = Tuple[str, str, str]
23
+
24
+ _HEALTH_URL = "https://meshcode.io/health"
25
+ _SUPABASE_URL = "https://supabase.co"
26
+
27
+
28
+ def _check_python() -> Status:
29
+ v = sys.version_info
30
+ if (v.major, v.minor) < (3, 9):
31
+ return (
32
+ "fail",
33
+ f"Python {v.major}.{v.minor}.{v.micro}",
34
+ "need Python >=3.9; install from python.org or `brew install python@3.13`",
35
+ )
36
+ # macOS Apple-stub detection: importing ssl on the stub fails
37
+ if sys.platform == "darwin":
38
+ try:
39
+ import ssl
40
+
41
+ _ = ssl.OPENSSL_VERSION
42
+ except Exception as e:
43
+ return (
44
+ "fail",
45
+ f"Python {v.major}.{v.minor}.{v.micro}",
46
+ f"Apple stub Python detected (ssl import failed: {e}). "
47
+ f"Install real Python: https://www.python.org/downloads/",
48
+ )
49
+ return ("ok", f"Python {v.major}.{v.minor}.{v.micro}", "")
50
+
51
+
52
+ def _check_ssl() -> Status:
53
+ try:
54
+ urllib.request.urlopen(_HEALTH_URL, timeout=5).read(64)
55
+ return ("ok", "SSL certs", "")
56
+ except urllib.error.URLError as e:
57
+ msg = str(e).lower()
58
+ if "certificate" in msg or "ssl" in msg:
59
+ fix = (
60
+ "run python's Install Certificates.command "
61
+ "(/Applications/Python <ver>/Install Certificates.command) "
62
+ "OR `pip install --upgrade certifi`"
63
+ )
64
+ return ("fail", "SSL certs", fix)
65
+ return ("warn", "SSL certs", f"network unreachable (skip if offline): {e}")
66
+ except Exception as e:
67
+ return ("warn", "SSL certs", f"check inconclusive: {e}")
68
+
69
+
70
+ def _check_meshcode_on_path() -> Status:
71
+ for tool in ("meshcode", "meshcode-mcp"):
72
+ if not shutil.which(tool):
73
+ v = sys.version_info
74
+ if sys.platform == "darwin":
75
+ hint = (
76
+ f'export PATH="$HOME/Library/Python/{v.major}.{v.minor}/bin:$PATH" '
77
+ f'(or run: python3 -m meshcode setup-path)'
78
+ )
79
+ else:
80
+ hint = (
81
+ "add Python user-base bin to PATH "
82
+ "(python3 -m site --user-base)/bin "
83
+ "or run: python3 -m meshcode setup-path"
84
+ )
85
+ return ("fail", f"`{tool}` on PATH", hint)
86
+ return ("ok", "meshcode CLI on PATH", "")
87
+
88
+
89
+ def _check_claude() -> Status:
90
+ if shutil.which("claude"):
91
+ return ("ok", "Claude Code installed", "")
92
+ return (
93
+ "fail",
94
+ "Claude Code installed",
95
+ "install from https://claude.ai/install "
96
+ "or `npm i -g @anthropic-ai/claude-code`",
97
+ )
98
+
99
+
100
+ def _check_node() -> Status:
101
+ needs_node = False
102
+ try:
103
+ from meshcode.preferences import load_prefs
104
+
105
+ cv = (load_prefs().get("claude_version") or "").lower()
106
+ if cv and cv not in ("latest", "none", "skip"):
107
+ needs_node = True
108
+ except Exception:
109
+ pass
110
+ if os.environ.get("MESHCODE_USE_NPX"):
111
+ needs_node = True
112
+ has_node = bool(shutil.which("node"))
113
+ has_npx = bool(shutil.which("npx"))
114
+ if has_node and has_npx:
115
+ return ("ok", "Node/npx", "")
116
+ level = "fail" if needs_node else "warn"
117
+ detail = (
118
+ "install Node: https://nodejs.org or `brew install node`"
119
+ if needs_node
120
+ else "only needed for `claude_version` pin or MESHCODE_USE_NPX; safe to skip"
121
+ )
122
+ return (level, "Node/npx", detail)
123
+
124
+
125
+ def _check_git() -> Status:
126
+ """git pre-flight with Apple stub detection + safe workarounds (B9)."""
127
+ git = shutil.which("git")
128
+ if not git:
129
+ return (
130
+ "fail",
131
+ "git",
132
+ "git not installed. Recommended (skip Apple's brittle modal):\n"
133
+ " - Manual CLT download: https://developer.apple.com/download/all/?q=command%20line%20tools\n"
134
+ " - OR Git for Mac: https://git-scm.com/download/mac\n"
135
+ " - OR `brew install git` (if Homebrew is installed)",
136
+ )
137
+ if sys.platform == "darwin":
138
+ try:
139
+ r = subprocess.run(
140
+ [git, "--version"],
141
+ capture_output=True,
142
+ text=True,
143
+ timeout=3,
144
+ )
145
+ stderr_lower = (r.stderr or "").lower()
146
+ if (
147
+ r.returncode != 0
148
+ or "command line developer tools" in stderr_lower
149
+ ):
150
+ return (
151
+ "fail",
152
+ "git",
153
+ "Apple stub git detected (would trigger CLT install modal). "
154
+ "Install real git: "
155
+ "https://developer.apple.com/download/all/?q=command%20line%20tools "
156
+ "OR `brew install git` OR https://git-scm.com/download/mac",
157
+ )
158
+ except subprocess.TimeoutExpired:
159
+ return (
160
+ "fail",
161
+ "git",
162
+ "git subprocess hung — likely the CLT install modal is "
163
+ "blocked or pending. Cancel any open Apple dialog; install "
164
+ "git manually: https://git-scm.com/download/mac",
165
+ )
166
+ except Exception:
167
+ pass
168
+ return ("ok", "git", "")
169
+
170
+
171
+ def _check_network() -> Status:
172
+ for url in (_HEALTH_URL, _SUPABASE_URL):
173
+ try:
174
+ urllib.request.urlopen(url, timeout=5).read(64)
175
+ except Exception as e:
176
+ return (
177
+ "warn",
178
+ "Network reachability",
179
+ f"{url} unreachable ({type(e).__name__}); "
180
+ f"check VPN/firewall if persistent",
181
+ )
182
+ return ("ok", "Network reachability", "")
183
+
184
+
185
+ def _check_api_key() -> Status:
186
+ try:
187
+ from meshcode.secrets import get_api_key, list_profiles
188
+
189
+ profiles = list_profiles() or []
190
+ if not profiles:
191
+ return (
192
+ "warn",
193
+ "API key in keychain",
194
+ "no profiles — run `meshcode init` to pair and store one",
195
+ )
196
+ for p in profiles:
197
+ if get_api_key(profile=p):
198
+ return ("ok", f"API key (profile={p})", "")
199
+ return (
200
+ "warn",
201
+ "API key in keychain",
202
+ f"profiles exist ({profiles}) but no key retrievable — "
203
+ f"`meshcode init` to re-pair",
204
+ )
205
+ except Exception as e:
206
+ return ("warn", "API key in keychain", f"secrets module unavailable: {e}")
207
+
208
+
209
+ def _check_prefs_json() -> Status:
210
+ pref = os.path.expanduser("~/.meshcode/preferences.json")
211
+ if not os.path.exists(pref):
212
+ return ("ok", "preferences.json", "(not created yet — default fine)")
213
+ try:
214
+ with open(pref) as f:
215
+ json.load(f)
216
+ return ("ok", "preferences.json", "")
217
+ except json.JSONDecodeError as e:
218
+ return (
219
+ "fail",
220
+ "preferences.json",
221
+ f"corrupt JSON ({e}); delete or fix: {pref}",
222
+ )
223
+ except Exception as e:
224
+ return ("warn", "preferences.json", f"unreadable: {e}")
225
+
226
+
227
+ CHECKS = [
228
+ _check_python,
229
+ _check_ssl,
230
+ _check_meshcode_on_path,
231
+ _check_claude,
232
+ _check_node,
233
+ _check_git,
234
+ _check_network,
235
+ _check_api_key,
236
+ _check_prefs_json,
237
+ ]
238
+
239
+
240
+ def run_doctor(skip_api_key_check: bool = False, quiet: bool = False) -> int:
241
+ """Run all checks, print results, return exit code (0 ok/warn, 1 fail)."""
242
+ results: List[Status] = []
243
+ for fn in CHECKS:
244
+ if skip_api_key_check and fn is _check_api_key:
245
+ continue
246
+ try:
247
+ results.append(fn())
248
+ except Exception as e:
249
+ results.append(("fail", fn.__name__.lstrip("_"), f"check crashed: {e}"))
250
+ blockers = 0
251
+ if not quiet:
252
+ print()
253
+ print(" meshcode doctor — environment check")
254
+ print(" " + "=" * 40)
255
+ for level, title, detail in results:
256
+ sym = {"ok": "✓", "warn": "⚠", "fail": "✗"}[level]
257
+ if level == "fail":
258
+ blockers += 1
259
+ line = f" {sym} {title}"
260
+ if detail:
261
+ line += f" — {detail}"
262
+ if not quiet:
263
+ print(line)
264
+ if not quiet:
265
+ print()
266
+ if blockers:
267
+ print(
268
+ f" {blockers} blocker(s) found — fix the above "
269
+ f"before `meshcode init`/`go`."
270
+ )
271
+ else:
272
+ print(" All checks passed (warnings non-blocking).")
273
+ return 1 if blockers else 0
274
+
275
+
276
+ if __name__ == "__main__":
277
+ sys.exit(run_doctor())
@@ -1699,61 +1699,109 @@ def _snapshot_pkg_mtimes():
1699
1699
  _PKG_PY_MTIMES = _snapshot_pkg_mtimes()
1700
1700
  log.info(f"install-guard: snapshotted {len(_PKG_PY_MTIMES)} *.py mtimes under {_PKG_DIR}")
1701
1701
 
1702
+ _INSTALL_GUARD_MODE = os.environ.get("MESHCODE_INSTALL_GUARD", "warn").lower()
1703
+ if _INSTALL_GUARD_MODE not in ("execv", "warn", "off"):
1704
+ log.warning(f"MESHCODE_INSTALL_GUARD={_INSTALL_GUARD_MODE!r} invalid; falling back to 'warn'")
1705
+ _INSTALL_GUARD_MODE = "warn"
1706
+ _INSTALL_GUARD_STALE_NOTIFIED = False
1707
+
1708
+
1709
+ def _is_agent_active() -> bool:
1710
+ """True if agent should NOT be execv-restarted right now."""
1711
+ if os.environ.get("MESHCODE_AGENT_ACTIVE", "").lower() in ("1", "true", "yes"):
1712
+ return True
1713
+ if _IN_WAIT:
1714
+ return True
1715
+ try:
1716
+ if (_time.time() - _last_tool_at) < 300:
1717
+ return True
1718
+ except Exception:
1719
+ pass
1720
+ return False
1721
+
1702
1722
 
1703
1723
  def _install_guard_check() -> None:
1704
- """Heartbeat-tick scan: if any *.py under the meshcode pkg path has
1705
- a fresher mtime than recorded at boot, the env was reinstalled while
1706
- the process is alive (class feedback_pip_install_into_live_mcp_env_self_kill).
1707
- The running image holds stale bytecode; lockfile collision and
1708
- orphan-watchdog will eventually kill us anyway. Pre-empt: release the
1709
- lease and execv-restart so the new wheel is picked up cleanly.
1710
-
1711
- The MCP socket to Claude Code WILL drop during the exec window.
1712
- Stop_hook _mcp_disconnected_without_reconnect (mig 68a3545e) catches
1713
- it; user runs `meshcode go <agent>` to pick up the new wheel.
1714
-
1715
- Soft-fail throughout: any exception here is logged and swallowed so
1716
- the heartbeat thread isn't taken down by a flaky filesystem call.
1724
+ """Heartbeat-tick scan: detect mid-session pkg reinstall.
1725
+
1726
+ Modes (MESHCODE_INSTALL_GUARD env var):
1727
+ - 'warn' (default): log + one-shot boot_version_drift msg; never execv.
1728
+ New wheel picked up on next cold boot. Removes mid-session surprise drops
1729
+ (RC of task 0c4524af 2026-05-19: pip install over live 2.11.7 sessions
1730
+ triggered execv on both backend+commander hb tick, dropping MCP socket).
1731
+ - 'execv': legacy auto-restart. Deferred to warn-mode when agent is active
1732
+ (in_wait, recent tool call <5min, or MESHCODE_AGENT_ACTIVE=1).
1733
+ - 'off': skip scan entirely.
1734
+
1735
+ Soft-fail throughout.
1717
1736
  """
1718
- if not _PKG_PY_MTIMES:
1737
+ global _INSTALL_GUARD_STALE_NOTIFIED
1738
+ if _INSTALL_GUARD_MODE == "off" or not _PKG_PY_MTIMES:
1719
1739
  return
1720
1740
  try:
1741
+ stale_path = None
1742
+ old_m = new_m = 0.0
1721
1743
  for path, old_mtime in list(_PKG_PY_MTIMES.items()):
1722
1744
  try:
1723
1745
  new_mtime = os.path.getmtime(path)
1724
1746
  except OSError:
1725
1747
  continue
1726
- # 0.5s slack — covers fs-timestamp resolution and tar extract
1727
- # re-emit jitter without false-positive on intentional touch.
1728
1748
  if new_mtime <= old_mtime + 0.5:
1729
1749
  continue
1750
+ stale_path = path
1751
+ old_m, new_m = old_mtime, new_mtime
1752
+ break
1753
+ if not stale_path:
1754
+ return
1755
+ if _INSTALL_GUARD_MODE == "execv" and not _is_agent_active():
1730
1756
  log.warning(
1731
- f"install-guard: self-modification detected on {path} "
1732
- f"(mtime {old_mtime:.0f} {new_mtime:.0f}). The running "
1733
- f"image holds stale bytecode; triggering clean execv-restart "
1734
- f"so the new wheel is picked up. MCP socket will drop briefly "
1735
- f"— stop_hook handles the agent release (mig 68a3545e). User "
1736
- f"runs `meshcode go <agent>` to relaunch under the new image. "
1737
- f"Class: feedback_pip_install_into_live_mcp_env_self_kill."
1757
+ f"install-guard: self-modification on {stale_path} "
1758
+ f"(mtime {old_m:.0f} -> {new_m:.0f}). Mode=execv, agent inactive "
1759
+ f"-- execv-restart. MCP socket will drop; stop_hook handles release."
1738
1760
  )
1739
1761
  try:
1740
1762
  _release_lease_synchronous(timeout_s=1.5)
1741
1763
  except Exception:
1742
1764
  pass
1743
1765
  _heartbeat_stop.set()
1744
- # execv replaces the current process; daemon threads die with
1745
- # it; new process inherits stdio. We pass the original argv so
1746
- # the entry-point command line is preserved verbatim.
1747
1766
  try:
1748
1767
  os.execv(sys.executable, [sys.executable] + sys.argv)
1749
1768
  except Exception as _exec_err:
1750
- log.error(
1751
- f"install-guard execv failed ({_exec_err}); "
1752
- f"falling back to os._exit(0). Lifespan finally + "
1753
- f"stop_hook still recover."
1754
- )
1769
+ log.error(f"install-guard execv failed ({_exec_err}); os._exit(0)")
1755
1770
  os._exit(0)
1756
- return # unreachable but explicit
1771
+ return
1772
+ if not _INSTALL_GUARD_STALE_NOTIFIED:
1773
+ _INSTALL_GUARD_STALE_NOTIFIED = True
1774
+ log.warning(
1775
+ f"install-guard: self-modification on {stale_path} "
1776
+ f"(mtime {old_m:.0f} -> {new_m:.0f}). Mode={_INSTALL_GUARD_MODE}, "
1777
+ f"agent_active={_is_agent_active()}. Running on STALE image -- new wheel "
1778
+ f"picked up on next `meshcode go <agent>` cold boot. "
1779
+ f"Set MESHCODE_INSTALL_GUARD=execv to opt into auto-restart."
1780
+ )
1781
+ try:
1782
+ import threading as _t
1783
+ def _notify():
1784
+ try:
1785
+ be.send_message(
1786
+ _PROJECT_ID, AGENT_NAME, "mesh-commander",
1787
+ {
1788
+ "type": "boot_version_drift",
1789
+ "subtype": "mid_session_stale_image",
1790
+ "path": stale_path,
1791
+ "old_mtime": old_m,
1792
+ "new_mtime": new_m,
1793
+ "agent": AGENT_NAME,
1794
+ "platform": sys.platform,
1795
+ "loaded_version": _SDK_VERSION,
1796
+ },
1797
+ msg_type="report",
1798
+ api_key=_get_api_key(),
1799
+ )
1800
+ except Exception as _e:
1801
+ log.debug(f"boot_version_drift notify failed (soft): {_e}")
1802
+ _t.Thread(target=_notify, daemon=True, name="meshcode-install-guard-notify").start()
1803
+ except Exception:
1804
+ pass
1757
1805
  except Exception as e:
1758
1806
  log.debug(f"install-guard scan failed (soft): {e}")
1759
1807
 
@@ -0,0 +1,75 @@
1
+ """Unit tests for the install-guard heartbeat scan.
2
+
3
+ RC for task 0c4524af (2026-05-19): pip install over live 2.11.7 sessions
4
+ triggered execv on both backend+commander hb tick, dropping MCP socket.
5
+ Fix (task d7df438e): default mode is 'warn' — log + one-shot notify, never
6
+ execv mid-session. Legacy execv path preserved as opt-in (MESHCODE_INSTALL_GUARD=execv)
7
+ and further deferred when agent is active.
8
+
9
+ Server module requires MESHCODE_PROJECT and MESHCODE_AGENT at import time
10
+ (L616-621). MESHCODE_PROJECT_ID short-circuits the network resolve path.
11
+ """
12
+ import os
13
+
14
+ os.environ.setdefault("MESHCODE_PROJECT", "test-install-guard")
15
+ os.environ.setdefault("MESHCODE_AGENT", "test-agent")
16
+ os.environ.setdefault("MESHCODE_PROJECT_ID", "00000000-0000-0000-0000-000000000000")
17
+ os.environ.setdefault("MESHCODE_API_KEY", "mc_test_fake_key_not_used")
18
+
19
+ import time
20
+ import pytest
21
+ from unittest import mock
22
+ from meshcode.meshcode_mcp import server as srv
23
+
24
+
25
+ @pytest.fixture
26
+ def stale_setup(monkeypatch, tmp_path):
27
+ fake = tmp_path / "fake_pkg.py"
28
+ fake.write_text("# stale")
29
+ old = fake.stat().st_mtime
30
+ monkeypatch.setattr(srv, "_PKG_PY_MTIMES", {str(fake): old - 100.0})
31
+ monkeypatch.setattr(srv, "_INSTALL_GUARD_STALE_NOTIFIED", False)
32
+ return fake
33
+
34
+
35
+ def test_default_warn_no_execv(stale_setup, monkeypatch):
36
+ monkeypatch.setattr(srv, "_INSTALL_GUARD_MODE", "warn")
37
+ with mock.patch.object(os, "execv") as me, mock.patch.object(os, "_exit") as mex:
38
+ srv._install_guard_check()
39
+ assert not me.called and not mex.called
40
+
41
+
42
+ def test_off_skips(stale_setup, monkeypatch):
43
+ monkeypatch.setattr(srv, "_INSTALL_GUARD_MODE", "off")
44
+ with mock.patch.object(os, "execv") as me:
45
+ srv._install_guard_check()
46
+ assert not me.called
47
+
48
+
49
+ def test_execv_inactive_fires(stale_setup, monkeypatch):
50
+ monkeypatch.setattr(srv, "_INSTALL_GUARD_MODE", "execv")
51
+ monkeypatch.setattr(srv, "_IN_WAIT", False)
52
+ monkeypatch.setattr(srv, "_last_tool_at", 0.0)
53
+ monkeypatch.delenv("MESHCODE_AGENT_ACTIVE", raising=False)
54
+ with mock.patch.object(os, "execv") as me, \
55
+ mock.patch.object(srv, "_release_lease_synchronous", create=True):
56
+ srv._install_guard_check()
57
+ assert me.called
58
+
59
+
60
+ def test_execv_active_defers(stale_setup, monkeypatch):
61
+ monkeypatch.setattr(srv, "_INSTALL_GUARD_MODE", "execv")
62
+ monkeypatch.setattr(srv, "_IN_WAIT", True)
63
+ with mock.patch.object(os, "execv") as me:
64
+ srv._install_guard_check()
65
+ assert not me.called
66
+
67
+
68
+ def test_one_shot_notify(stale_setup, monkeypatch):
69
+ monkeypatch.setattr(srv, "_INSTALL_GUARD_MODE", "warn")
70
+ sends = []
71
+ monkeypatch.setattr(srv.be, "send_message", lambda *a, **kw: sends.append((a, kw)))
72
+ srv._install_guard_check(); time.sleep(0.3)
73
+ n1 = len(sends)
74
+ srv._install_guard_check(); time.sleep(0.3)
75
+ assert len(sends) == n1, "second call must not duplicate notify"
@@ -0,0 +1,82 @@
1
+ """Regression test for B1 (task f1d34641) — friction #9.
2
+
3
+ `meshcode prefs claude-version latest` must save "latest" literally instead
4
+ of deleting the key. The disable sentinel "latest"/"none"/"skip" is read at
5
+ run_agent.py:475 to skip the ESC pin; if the CLI deletes the key, the user
6
+ falls back to the hardcoded 2.1.104 pin → npx fails for users without Node.
7
+
8
+ Bug location: comms_v4.py:3396-3416 (pre-fix grouped `latest` with
9
+ `reset`/`unpin` and popped the key).
10
+ """
11
+ import json
12
+ import os
13
+
14
+ os.environ.setdefault("MESHCODE_PROJECT", "test-prefs-b1")
15
+ os.environ.setdefault("MESHCODE_AGENT", "test-agent")
16
+
17
+ import pytest
18
+
19
+
20
+ @pytest.fixture
21
+ def isolated_prefs(tmp_path, monkeypatch):
22
+ pref_file = tmp_path / "preferences.json"
23
+ from meshcode import preferences
24
+ monkeypatch.setattr(preferences, "PREFS_PATH", pref_file)
25
+ return pref_file
26
+
27
+
28
+ def _apply_branch(ver: str):
29
+ """Mirror the post-fix branch in comms_v4.py:3396-3420."""
30
+ from meshcode.preferences import load_prefs, save_prefs
31
+ if ver in ("reset", "unpin"):
32
+ prefs = load_prefs()
33
+ prefs.pop("claude_version", None)
34
+ save_prefs(prefs)
35
+ elif ver in ("latest", "none", "skip"):
36
+ prefs = load_prefs()
37
+ prefs["claude_version"] = ver
38
+ save_prefs(prefs)
39
+ else:
40
+ prefs = load_prefs()
41
+ prefs["claude_version"] = ver
42
+ save_prefs(prefs)
43
+
44
+
45
+ def test_claude_version_latest_saves_literal(isolated_prefs):
46
+ """Repro for friction #9: `meshcode prefs claude-version latest` must
47
+ save "latest" to JSON, NOT delete the key."""
48
+ from meshcode.preferences import save_prefs
49
+ save_prefs({"claude_version": "2.1.50"})
50
+ _apply_branch("latest")
51
+ data = json.loads(isolated_prefs.read_text())
52
+ assert data.get("claude_version") == "latest", \
53
+ f"expected 'latest' literal, got {data!r}"
54
+
55
+
56
+ def test_claude_version_none_saves_literal(isolated_prefs):
57
+ _apply_branch("none")
58
+ assert json.loads(isolated_prefs.read_text()).get("claude_version") == "none"
59
+
60
+
61
+ def test_claude_version_skip_saves_literal(isolated_prefs):
62
+ _apply_branch("skip")
63
+ assert json.loads(isolated_prefs.read_text()).get("claude_version") == "skip"
64
+
65
+
66
+ def test_claude_version_reset_clears_key(isolated_prefs):
67
+ from meshcode.preferences import save_prefs, load_prefs
68
+ save_prefs({"claude_version": "2.1.50"})
69
+ _apply_branch("reset")
70
+ assert "claude_version" not in load_prefs()
71
+
72
+
73
+ def test_claude_version_unpin_clears_key(isolated_prefs):
74
+ from meshcode.preferences import save_prefs, load_prefs
75
+ save_prefs({"claude_version": "2.1.50"})
76
+ _apply_branch("unpin")
77
+ assert "claude_version" not in load_prefs()
78
+
79
+
80
+ def test_claude_version_pin_saves_specific_version(isolated_prefs):
81
+ _apply_branch("2.1.108")
82
+ assert json.loads(isolated_prefs.read_text()).get("claude_version") == "2.1.108"