meshcode 2.11.107__tar.gz → 2.11.109__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 (257) hide show
  1. {meshcode-2.11.107 → meshcode-2.11.109}/PKG-INFO +1 -1
  2. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/__init__.py +1 -1
  3. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/hostd.py +163 -2
  4. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/meshcode_mcp/server.py +13 -18
  5. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/protocol_handler.py +141 -0
  6. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/run_agent.py +31 -0
  7. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/self_update.py +50 -7
  8. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/setup_clients.py +5 -0
  9. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode.egg-info/PKG-INFO +1 -1
  10. meshcode-2.11.109/meshcode.egg-info/SOURCES.txt +97 -0
  11. meshcode-2.11.109/meshcode.egg-info/top_level.txt +1 -0
  12. {meshcode-2.11.107 → meshcode-2.11.109}/pyproject.toml +2 -2
  13. {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_wait_open_tasks_contradiction.py +3 -2
  14. meshcode-2.11.107/meshcode/cli.py +0 -42
  15. meshcode-2.11.107/meshcode/compat.py +0 -174
  16. meshcode-2.11.107/meshcode/error_hints.py +0 -74
  17. meshcode-2.11.107/meshcode/exceptions.py +0 -52
  18. meshcode-2.11.107/meshcode/invites.py +0 -406
  19. meshcode-2.11.107/meshcode/launcher.py +0 -353
  20. meshcode-2.11.107/meshcode/launcher_install.py +0 -414
  21. meshcode-2.11.107/meshcode/meshcode_mcp/__init__.py +0 -22
  22. meshcode-2.11.107/meshcode/meshcode_mcp/__main__.py +0 -62
  23. meshcode-2.11.107/meshcode/meshcode_mcp/test_backend.py +0 -86
  24. meshcode-2.11.107/meshcode/meshcode_mcp/test_realtime.py +0 -95
  25. meshcode-2.11.107/meshcode/meshcode_mcp/test_server_wrapper.py +0 -117
  26. meshcode-2.11.107/meshcode/preferences.py +0 -260
  27. meshcode-2.11.107/meshcode/protocol_v2.py +0 -129
  28. meshcode-2.11.107/meshcode/secrets.py +0 -365
  29. meshcode-2.11.107/meshcode/supervisor.py +0 -186
  30. meshcode-2.11.107/meshcode/upload.py +0 -125
  31. meshcode-2.11.107/meshcode-backend-wt/comms_v4.py +0 -1941
  32. meshcode-2.11.107/meshcode-backend-wt/meshcode/__init__.py +0 -82
  33. meshcode-2.11.107/meshcode-backend-wt/meshcode/ascii_art.py +0 -638
  34. meshcode-2.11.107/meshcode-backend-wt/meshcode/comms_v4.py +0 -3563
  35. meshcode-2.11.107/meshcode-backend-wt/meshcode/meshcode_mcp/backend.py +0 -1261
  36. meshcode-2.11.107/meshcode-backend-wt/meshcode/meshcode_mcp/realtime.py +0 -460
  37. meshcode-2.11.107/meshcode-backend-wt/meshcode/meshcode_mcp/server.py +0 -4117
  38. meshcode-2.11.107/meshcode-backend-wt/meshcode/quickstart.py +0 -148
  39. meshcode-2.11.107/meshcode-backend-wt/meshcode/run_agent.py +0 -958
  40. meshcode-2.11.107/meshcode-backend-wt/meshcode/self_update.py +0 -345
  41. meshcode-2.11.107/meshcode-backend-wt/meshcode/setup_clients.py +0 -926
  42. meshcode-2.11.107/meshcode-backend-wt/scripts/sentinel.py +0 -257
  43. meshcode-2.11.107/meshcode-backend-wt/tests/test_rpc_migrations.py +0 -387
  44. meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/__init__.py +0 -82
  45. meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/ascii_art.py +0 -638
  46. meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/cli.py +0 -42
  47. meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/comms_v4.py +0 -3563
  48. meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/compat.py +0 -174
  49. meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/error_hints.py +0 -74
  50. meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/exceptions.py +0 -52
  51. meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/invites.py +0 -406
  52. meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/launcher.py +0 -353
  53. meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/launcher_install.py +0 -414
  54. meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/__init__.py +0 -22
  55. meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/__main__.py +0 -62
  56. meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/backend.py +0 -1261
  57. meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/realtime.py +0 -460
  58. meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/server.py +0 -4117
  59. meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/test_backend.py +0 -86
  60. meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/test_realtime.py +0 -95
  61. meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/test_server_wrapper.py +0 -117
  62. meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/preferences.py +0 -260
  63. meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/protocol_v2.py +0 -129
  64. meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/quickstart.py +0 -148
  65. meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/run_agent.py +0 -958
  66. meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/secrets.py +0 -365
  67. meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/self_update.py +0 -345
  68. meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/setup_clients.py +0 -926
  69. meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/supervisor.py +0 -186
  70. meshcode-2.11.107/meshcode-noun-wt/build/lib/meshcode/upload.py +0 -125
  71. meshcode-2.11.107/meshcode-noun-wt/comms_v4.py +0 -1941
  72. meshcode-2.11.107/meshcode-noun-wt/meshcode/__init__.py +0 -82
  73. meshcode-2.11.107/meshcode-noun-wt/meshcode/ascii_art.py +0 -638
  74. meshcode-2.11.107/meshcode-noun-wt/meshcode/cli.py +0 -42
  75. meshcode-2.11.107/meshcode-noun-wt/meshcode/comms_v4.py +0 -3563
  76. meshcode-2.11.107/meshcode-noun-wt/meshcode/compat.py +0 -174
  77. meshcode-2.11.107/meshcode-noun-wt/meshcode/error_hints.py +0 -74
  78. meshcode-2.11.107/meshcode-noun-wt/meshcode/exceptions.py +0 -52
  79. meshcode-2.11.107/meshcode-noun-wt/meshcode/invites.py +0 -406
  80. meshcode-2.11.107/meshcode-noun-wt/meshcode/launcher.py +0 -353
  81. meshcode-2.11.107/meshcode-noun-wt/meshcode/launcher_install.py +0 -414
  82. meshcode-2.11.107/meshcode-noun-wt/meshcode/meshcode_mcp/__init__.py +0 -22
  83. meshcode-2.11.107/meshcode-noun-wt/meshcode/meshcode_mcp/__main__.py +0 -62
  84. meshcode-2.11.107/meshcode-noun-wt/meshcode/meshcode_mcp/backend.py +0 -1261
  85. meshcode-2.11.107/meshcode-noun-wt/meshcode/meshcode_mcp/realtime.py +0 -460
  86. meshcode-2.11.107/meshcode-noun-wt/meshcode/meshcode_mcp/server.py +0 -4117
  87. meshcode-2.11.107/meshcode-noun-wt/meshcode/meshcode_mcp/test_backend.py +0 -86
  88. meshcode-2.11.107/meshcode-noun-wt/meshcode/meshcode_mcp/test_realtime.py +0 -95
  89. meshcode-2.11.107/meshcode-noun-wt/meshcode/meshcode_mcp/test_server_wrapper.py +0 -117
  90. meshcode-2.11.107/meshcode-noun-wt/meshcode/preferences.py +0 -260
  91. meshcode-2.11.107/meshcode-noun-wt/meshcode/protocol_v2.py +0 -129
  92. meshcode-2.11.107/meshcode-noun-wt/meshcode/quickstart.py +0 -148
  93. meshcode-2.11.107/meshcode-noun-wt/meshcode/run_agent.py +0 -958
  94. meshcode-2.11.107/meshcode-noun-wt/meshcode/secrets.py +0 -365
  95. meshcode-2.11.107/meshcode-noun-wt/meshcode/self_update.py +0 -345
  96. meshcode-2.11.107/meshcode-noun-wt/meshcode/setup_clients.py +0 -926
  97. meshcode-2.11.107/meshcode-noun-wt/meshcode/supervisor.py +0 -186
  98. meshcode-2.11.107/meshcode-noun-wt/meshcode/upload.py +0 -125
  99. meshcode-2.11.107/meshcode-noun-wt/scripts/sentinel.py +0 -257
  100. meshcode-2.11.107/meshcode-noun-wt/tests/test_core.py +0 -216
  101. meshcode-2.11.107/meshcode-noun-wt/tests/test_cross_agent_messaging.py +0 -366
  102. meshcode-2.11.107/meshcode-noun-wt/tests/test_esc_deaf_state.py +0 -361
  103. meshcode-2.11.107/meshcode-noun-wt/tests/test_exceptions.py +0 -107
  104. meshcode-2.11.107/meshcode-noun-wt/tests/test_mark_read_batch.py +0 -200
  105. meshcode-2.11.107/meshcode-noun-wt/tests/test_migration_integrity.py +0 -176
  106. meshcode-2.11.107/meshcode-noun-wt/tests/test_realtime_event_freshness.py +0 -236
  107. meshcode-2.11.107/meshcode-noun-wt/tests/test_rls_cross_tenant.py +0 -255
  108. meshcode-2.11.107/meshcode-noun-wt/tests/test_rpc_migrations.py +0 -387
  109. meshcode-2.11.107/meshcode-noun-wt/tests/test_security_regressions.py +0 -171
  110. meshcode-2.11.107/meshcode-noun-wt/tests/test_sentinel.py +0 -148
  111. meshcode-2.11.107/meshcode-noun-wt/tests/test_status_enum_coverage.py +0 -231
  112. meshcode-2.11.107/meshcode-tasks-wt/comms_v4.py +0 -1941
  113. meshcode-2.11.107/meshcode-tasks-wt/meshcode/__init__.py +0 -82
  114. meshcode-2.11.107/meshcode-tasks-wt/meshcode/ascii_art.py +0 -638
  115. meshcode-2.11.107/meshcode-tasks-wt/meshcode/cli.py +0 -42
  116. meshcode-2.11.107/meshcode-tasks-wt/meshcode/comms_v4.py +0 -3563
  117. meshcode-2.11.107/meshcode-tasks-wt/meshcode/compat.py +0 -174
  118. meshcode-2.11.107/meshcode-tasks-wt/meshcode/error_hints.py +0 -74
  119. meshcode-2.11.107/meshcode-tasks-wt/meshcode/exceptions.py +0 -52
  120. meshcode-2.11.107/meshcode-tasks-wt/meshcode/invites.py +0 -406
  121. meshcode-2.11.107/meshcode-tasks-wt/meshcode/launcher.py +0 -353
  122. meshcode-2.11.107/meshcode-tasks-wt/meshcode/launcher_install.py +0 -414
  123. meshcode-2.11.107/meshcode-tasks-wt/meshcode/meshcode_mcp/__init__.py +0 -22
  124. meshcode-2.11.107/meshcode-tasks-wt/meshcode/meshcode_mcp/__main__.py +0 -62
  125. meshcode-2.11.107/meshcode-tasks-wt/meshcode/meshcode_mcp/backend.py +0 -1261
  126. meshcode-2.11.107/meshcode-tasks-wt/meshcode/meshcode_mcp/realtime.py +0 -460
  127. meshcode-2.11.107/meshcode-tasks-wt/meshcode/meshcode_mcp/server.py +0 -4117
  128. meshcode-2.11.107/meshcode-tasks-wt/meshcode/meshcode_mcp/test_backend.py +0 -86
  129. meshcode-2.11.107/meshcode-tasks-wt/meshcode/meshcode_mcp/test_realtime.py +0 -95
  130. meshcode-2.11.107/meshcode-tasks-wt/meshcode/meshcode_mcp/test_server_wrapper.py +0 -117
  131. meshcode-2.11.107/meshcode-tasks-wt/meshcode/preferences.py +0 -260
  132. meshcode-2.11.107/meshcode-tasks-wt/meshcode/protocol_v2.py +0 -129
  133. meshcode-2.11.107/meshcode-tasks-wt/meshcode/quickstart.py +0 -148
  134. meshcode-2.11.107/meshcode-tasks-wt/meshcode/run_agent.py +0 -958
  135. meshcode-2.11.107/meshcode-tasks-wt/meshcode/secrets.py +0 -365
  136. meshcode-2.11.107/meshcode-tasks-wt/meshcode/self_update.py +0 -345
  137. meshcode-2.11.107/meshcode-tasks-wt/meshcode/setup_clients.py +0 -926
  138. meshcode-2.11.107/meshcode-tasks-wt/meshcode/supervisor.py +0 -186
  139. meshcode-2.11.107/meshcode-tasks-wt/meshcode/upload.py +0 -125
  140. meshcode-2.11.107/meshcode-tasks-wt/scripts/sentinel.py +0 -257
  141. meshcode-2.11.107/meshcode-tasks-wt/tests/test_core.py +0 -216
  142. meshcode-2.11.107/meshcode-tasks-wt/tests/test_cross_agent_messaging.py +0 -366
  143. meshcode-2.11.107/meshcode-tasks-wt/tests/test_esc_deaf_state.py +0 -361
  144. meshcode-2.11.107/meshcode-tasks-wt/tests/test_exceptions.py +0 -107
  145. meshcode-2.11.107/meshcode-tasks-wt/tests/test_mark_read_batch.py +0 -200
  146. meshcode-2.11.107/meshcode-tasks-wt/tests/test_migration_integrity.py +0 -176
  147. meshcode-2.11.107/meshcode-tasks-wt/tests/test_realtime_event_freshness.py +0 -236
  148. meshcode-2.11.107/meshcode-tasks-wt/tests/test_rls_cross_tenant.py +0 -255
  149. meshcode-2.11.107/meshcode-tasks-wt/tests/test_rpc_migrations.py +0 -387
  150. meshcode-2.11.107/meshcode-tasks-wt/tests/test_security_regressions.py +0 -171
  151. meshcode-2.11.107/meshcode-tasks-wt/tests/test_sentinel.py +0 -148
  152. meshcode-2.11.107/meshcode-tasks-wt/tests/test_status_enum_coverage.py +0 -231
  153. meshcode-2.11.107/meshcode.egg-info/PKG-INFO 2 +0 -460
  154. meshcode-2.11.107/meshcode.egg-info/SOURCES 2.txt +0 -239
  155. meshcode-2.11.107/meshcode.egg-info/SOURCES.txt +0 -253
  156. meshcode-2.11.107/meshcode.egg-info/dependency_links 2.txt +0 -1
  157. meshcode-2.11.107/meshcode.egg-info/entry_points 2.txt +0 -3
  158. meshcode-2.11.107/meshcode.egg-info/requires 2.txt +0 -13
  159. meshcode-2.11.107/meshcode.egg-info/top_level 2.txt +0 -4
  160. meshcode-2.11.107/meshcode.egg-info/top_level.txt +0 -4
  161. meshcode-2.11.107/tests/test_core.py +0 -216
  162. meshcode-2.11.107/tests/test_cross_agent_messaging.py +0 -366
  163. meshcode-2.11.107/tests/test_esc_deaf_state.py +0 -361
  164. meshcode-2.11.107/tests/test_exceptions.py +0 -107
  165. meshcode-2.11.107/tests/test_mark_read_batch.py +0 -200
  166. meshcode-2.11.107/tests/test_migration_integrity.py +0 -176
  167. meshcode-2.11.107/tests/test_realtime_event_freshness.py +0 -236
  168. meshcode-2.11.107/tests/test_rls_cross_tenant.py +0 -255
  169. meshcode-2.11.107/tests/test_security_regressions.py +0 -171
  170. meshcode-2.11.107/tests/test_sentinel.py +0 -148
  171. meshcode-2.11.107/tests/test_status_enum_coverage.py +0 -231
  172. {meshcode-2.11.107 → meshcode-2.11.109}/README.md +0 -0
  173. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/__main__.py +0 -0
  174. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/_session_handoff_template 2.py +0 -0
  175. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/_session_handoff_template 3.py +0 -0
  176. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/_session_handoff_template.py +0 -0
  177. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/_stop_hook_template.py +0 -0
  178. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/ascii_art.py +0 -0
  179. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/atomic_push.py +0 -0
  180. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/claude_update 2.py +0 -0
  181. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/claude_update 3.py +0 -0
  182. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/claude_update.py +0 -0
  183. {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/meshcode/cli.py +0 -0
  184. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/comms_v4.py +0 -0
  185. {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/meshcode/compat.py +0 -0
  186. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/daemon.py +0 -0
  187. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/date_parse.py +0 -0
  188. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/doctor.py +0 -0
  189. {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/meshcode/error_hints.py +0 -0
  190. {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/meshcode/exceptions.py +0 -0
  191. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/hostd 2.py +0 -0
  192. {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/meshcode/invites.py +0 -0
  193. {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/meshcode/launcher.py +0 -0
  194. {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/meshcode/launcher_install.py +0 -0
  195. {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/meshcode/meshcode_mcp/__init__.py +0 -0
  196. {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/meshcode/meshcode_mcp/__main__.py +0 -0
  197. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/meshcode_mcp/backend.py +0 -0
  198. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/meshcode_mcp/realtime.py +0 -0
  199. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
  200. {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/meshcode/meshcode_mcp/test_backend.py +0 -0
  201. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
  202. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
  203. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
  204. {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  205. {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  206. {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/meshcode/preferences.py +0 -0
  207. {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/meshcode/protocol_v2.py +0 -0
  208. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/quickstart.py +0 -0
  209. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/rpc_allowlist.py +0 -0
  210. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/scripts/check_secrets.py +0 -0
  211. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/scripts/race_rate_harness.py +0 -0
  212. {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/meshcode/secrets.py +0 -0
  213. {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/meshcode/supervisor.py +0 -0
  214. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/up 2.py +0 -0
  215. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode/up.py +0 -0
  216. {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/meshcode/upload.py +0 -0
  217. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode.egg-info/dependency_links.txt +0 -0
  218. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode.egg-info/entry_points.txt +0 -0
  219. {meshcode-2.11.107 → meshcode-2.11.109}/meshcode.egg-info/requires.txt +0 -0
  220. {meshcode-2.11.107 → meshcode-2.11.109}/setup.cfg +0 -0
  221. {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_auto_update_hardening.py +0 -0
  222. {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_autonomous_closegap_1.py +0 -0
  223. {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_autonomous_closegap_2.py +0 -0
  224. {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_autonomous_closegap_3.py +0 -0
  225. {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_autonomous_prompt_inject 2.py +0 -0
  226. {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_autonomous_prompt_inject 3.py +0 -0
  227. {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_autonomous_prompt_inject.py +0 -0
  228. {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_boot_bug_regression.py +0 -0
  229. {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_color_truecolor.py +0 -0
  230. {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/tests/test_core.py +0 -0
  231. {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/tests/test_cross_agent_messaging.py +0 -0
  232. {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_date_parse.py +0 -0
  233. {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_doctor.py +0 -0
  234. {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_epistemic_v1_python_sdk.py +0 -0
  235. {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_epistemic_v1_stop_conditions.py +0 -0
  236. {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/tests/test_esc_deaf_state.py +0 -0
  237. {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/tests/test_exceptions.py +0 -0
  238. {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_file_upload.py +0 -0
  239. {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_init_device_code.py +0 -0
  240. {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_install_guard.py +0 -0
  241. {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_lease_sigterm_release.py +0 -0
  242. {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/tests/test_mark_read_batch.py +0 -0
  243. {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_marketplace_ratings.py +0 -0
  244. {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/tests/test_migration_integrity.py +0 -0
  245. {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/tests/test_realtime_event_freshness.py +0 -0
  246. {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/tests/test_rls_cross_tenant.py +0 -0
  247. {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_rpc_grants.py +0 -0
  248. {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_rpc_migrations.py +0 -0
  249. {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_run_agent_dry_run.py +0 -0
  250. {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_run_agent_no_server_import.py +0 -0
  251. {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/tests/test_security_regressions.py +0 -0
  252. {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_self_update_user_site.py +0 -0
  253. {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/tests/test_sentinel.py +0 -0
  254. {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_setup_path.py +0 -0
  255. {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_sleep_signals.py +0 -0
  256. {meshcode-2.11.107/meshcode-backend-wt → meshcode-2.11.109}/tests/test_status_enum_coverage.py +0 -0
  257. {meshcode-2.11.107 → meshcode-2.11.109}/tests/test_stay_on_loop_hook.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.11.107
3
+ Version: 2.11.109
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.107"
2
+ __version__ = "2.11.109"
3
3
 
4
4
  # Exception hierarchy — eagerly imported (lightweight, no deps)
5
5
  from meshcode.exceptions import ( # noqa: F401
@@ -503,6 +503,48 @@ def _do_respawns(api_key: str, host_id: str) -> int:
503
503
  # only guard; for crash respawns it's a harmless backstop (the server
504
504
  # rate-limit at mig404 is stricter and trips first).
505
505
  _target = f"{proj}/{agent}"
506
+ # CONVERGENCE GUARD (451d33a0) — recycle fast-path ONLY. Stops the env-mismatch
507
+ # recycle-RESPAWN storm the version-recycle guard can't see (see _recycle_blocked).
508
+ # Counts consecutive recycle-respawns of this target; at _RECYREC_MAX within the
509
+ # window it blocks relaunch + alerts once, instead of storming terminals forever.
510
+ if _is_recycle:
511
+ _rst = _load_state()
512
+ _rblk = _recycle_blocked(_rst, _target)
513
+ if _rblk:
514
+ _log(f"SKIP recycle-respawn {_target}: BLOCKED ({_rblk}) — non-converging recycle; "
515
+ f"holding relaunch (auto-retry after {_RECYREC_BLOCK_TTL_S}s or a manual Start).")
516
+ continue
517
+ _now = time.time()
518
+ _rrall = dict(_rst.get("recyrespawn") or {})
519
+ _rr = dict(_rrall.get(_target) or {})
520
+ if _rr.get("last_ts") and (_now - float(_rr["last_ts"])) <= _RECYREC_WINDOW_S:
521
+ _rr["count"] = int(_rr.get("count", 0)) + 1
522
+ else:
523
+ _rr = {"count": 1} # window elapsed (converged/idle) -> fresh count, clears any stale block
524
+ _rr["last_ts"] = _now
525
+ if _rr["count"] >= _RECYREC_MAX:
526
+ _rr["blocked_ts"] = _now
527
+ _rr["reason"] = "recycle_no_converge"
528
+ _rrall[_target] = _rr
529
+ _rst["recyrespawn"] = _rrall
530
+ _save_state(_rst)
531
+ _log(f"RECYCLE-STUCK {_target}: {_rr['count']} recycle-respawns in <={_RECYREC_WINDOW_S}s "
532
+ f"without converging — spawn env likely older than hostd; align it "
533
+ f"(e.g. ~/meshcode-env/bin/pip install -U meshcode==<disk>). BLOCKING recycle-respawns "
534
+ f"for {_RECYREC_BLOCK_TTL_S}s. [recycle_blocked_reason=recycle_no_converge]")
535
+ try: # stop the dashboard's eternal 'launching…' spinner (same pattern as the breaker)
536
+ _rpc("mc_resolve_launch", {
537
+ "p_api_key": api_key, "p_project_id": c.get("project_id"), "p_agent": agent,
538
+ "p_status": "failed", "p_reason": "recycle_no_converge",
539
+ "p_detail": "recycle keeps relaunching without converging — this agent's MCP env is "
540
+ "older than the host; align it (pip install -U meshcode==<host version>), "
541
+ "then Start again"})
542
+ except Exception:
543
+ pass
544
+ continue
545
+ _rrall[_target] = _rr
546
+ _rst["recyrespawn"] = _rrall
547
+ _save_state(_rst)
506
548
  _ok, _burst, _why = _spawn_rate_ok(_target)
507
549
  if not _ok:
508
550
  _log(f"SKIP {'recycle-' if _is_recycle else ''}respawn {_target}: rate-limited ({_why})")
@@ -519,11 +561,17 @@ def _do_respawns(api_key: str, host_id: str) -> int:
519
561
  except Exception:
520
562
  pass
521
563
  continue
564
+ # Part 2 (Samuel req #2): for a VISIBLE recycle, snapshot the OLD window
565
+ # pid(s) BEFORE spawning the fresh one — so the new pid is never in the
566
+ # close set (commander q1: never touch the fresh terminal).
567
+ _old_vis_pids = _discover_agent_pids(_target) if (_is_recycle and _visible) else []
522
568
  _log(f"{'RECYCLE-RESPAWN' if _is_recycle else 'RESPAWN'} {proj}/{agent} "
523
569
  f"({'VISIBLE ' if _visible else ''}stale {c.get('heartbeat_age_s')}s, count={c.get('respawn_count')})")
524
570
  if _spawn_agent(proj, agent, headless=_hl):
525
571
  _record_spawn(_target) # count the terminal we just opened, against the breaker
526
572
  if _is_recycle:
573
+ if _visible and _old_vis_pids:
574
+ _close_old_visible_recycle(_target, _old_vis_pids) # close old window (DRY-RUN first)
527
575
  _rpc("mc_record_recycle",
528
576
  {"p_api_key": api_key, "p_project_id": c.get("project_id"), "p_agent_name": agent})
529
577
  n += 1
@@ -591,6 +639,103 @@ def _save_state(st: dict) -> None:
591
639
  pass
592
640
 
593
641
 
642
+ # ------------------------------------------------------------------
643
+ # Recycle non-convergence guard (451d33a0). The env-mismatch storm is a recycle-
644
+ # RESPAWN loop (log: "RECYCLE-RESPAWN <agent> (stale 15-37s, count=0)" every
645
+ # ~15-37s) that the version-recycle guard in _do_version_recycles can NOT see —
646
+ # that guard sits on the version-recycle REQUEST path and its counter only
647
+ # advances on a successful mc_request_recycle, so when a recycle is already
648
+ # pending (requested=False) the counter stays 0 and never trips. This guard sits
649
+ # on the recycle fast-path itself: if a target recycle-respawns _RECYREC_MAX times
650
+ # within _RECYREC_WINDOW_S without converging, STOP relaunching it + alert once
651
+ # (env mismatch) instead of storming terminals forever. Self-heals — the block
652
+ # lifts after _RECYREC_BLOCK_TTL_S (one clean retry; re-blocks if it storms again),
653
+ # and the counter resets the moment the target stops re-qualifying (converged).
654
+ _RECYREC_MAX = _env_int("MESHCODE_RECYREC_MAX", 3, 2) # recycle-respawns before non-converge
655
+ _RECYREC_WINDOW_S = _env_int("MESHCODE_RECYREC_WINDOW_SEC", 120, 30) # consecutive-respawn counting window
656
+ _RECYREC_BLOCK_TTL_S = _env_int("MESHCODE_RECYREC_BLOCK_TTL_SEC", 600, 60) # block duration before a retry
657
+
658
+
659
+ def _recycle_blocked(st, target, now=None):
660
+ """Shared guard consulted by BOTH _do_respawns (skip the recycle fast-path)
661
+ and _do_version_recycles (don't version-recycle a non-converging target).
662
+ Returns a reason string while `target` is blocked, else None. The block
663
+ auto-expires after _RECYREC_BLOCK_TTL_S so a fixed env gets one clean retry."""
664
+ now = now if now is not None else time.time()
665
+ rec = (st.get("recyrespawn") or {}).get(target) or {}
666
+ bts = rec.get("blocked_ts")
667
+ if bts:
668
+ try:
669
+ if (now - float(bts)) < _RECYREC_BLOCK_TTL_S:
670
+ return rec.get("reason") or "recycle_no_converge"
671
+ except (TypeError, ValueError):
672
+ return None
673
+ return None
674
+
675
+
676
+ # Samuel rule 2026-06-04: never recycle a CONNECTED agent (live MCP session) except
677
+ # the >3h uptime lifecycle. BUSY_STATUSES (working/online/busy) MISSES a connected-
678
+ # but-idle agent (status idle/standby, window open, heartbeat fresh) — a fresh
679
+ # heartbeat is the stronger 'live session' signal, so an idle-but-connected agent
680
+ # was being version-recycled out from under the user (the storm he kept seeing).
681
+ _CONNECTED_HEARTBEAT_S = _env_int("MESHCODE_CONNECTED_HEARTBEAT_SEC", 60, 10)
682
+
683
+
684
+ def _agent_connected(a) -> bool:
685
+ """True if the agent has a live MCP session: an explicitly-busy status OR a
686
+ very-recent heartbeat (window open even when idle/standby)."""
687
+ if (a.get("status") or "") in BUSY_STATUSES:
688
+ return True
689
+ hb = a.get("heartbeat_age_s")
690
+ try:
691
+ return hb is not None and float(hb) < _CONNECTED_HEARTBEAT_S
692
+ except (TypeError, ValueError):
693
+ return False
694
+
695
+
696
+ # Part 2 (Samuel req #2 2026-06-04): on a VISIBLE recycle, close the OLD window so
697
+ # old+new don't both stay open (audit gap 6a203baa). DRY-RUN first (commander q2 +
698
+ # reaper safe-arm pattern): log-only until the logs confirm it's ONLY the old pid.
699
+ _CLOSE_OLD_VISIBLE_DRYRUN = True
700
+
701
+
702
+ def _pid_alive(pid) -> bool:
703
+ if not pid:
704
+ return False
705
+ try:
706
+ os.kill(int(pid), 0)
707
+ return True
708
+ except ProcessLookupError:
709
+ return False
710
+ except PermissionError:
711
+ return True # exists, owned by another uid — treat as alive (don't guess)
712
+ except Exception:
713
+ return False
714
+
715
+
716
+ def _close_old_visible_recycle(target: str, old_pids) -> int:
717
+ """Close the OLD window's still-alive process on a VISIBLE recycle. `old_pids`
718
+ is the PRE-SPAWN snapshot, so the freshly-opened window's pid is excluded by
719
+ construction — we NEVER touch the fresh terminal (commander q1). Graceful
720
+ self-exit (the stop-hook ends the session on must_exit=recycle) is PRIMARY
721
+ (q3): an already-exited old pid is skipped. DRY-RUN first (q2): log the
722
+ would-close pid; flip _CLOSE_OLD_VISIBLE_DRYRUN=False to arm once logs show
723
+ it's only the old pid. Real kill reuses _kill_headless_pid's cmdline guard."""
724
+ n = 0
725
+ for pid in old_pids:
726
+ if not _pid_alive(pid):
727
+ continue # already self-closed gracefully (q3 primary) — nothing to do
728
+ if _CLOSE_OLD_VISIBLE_DRYRUN:
729
+ _log(f"CLOSE-OLD-VISIBLE-DRYRUN {target}: WOULD close old window pid {pid} "
730
+ f"(visible recycle; fresh window already spawned + excluded) — log-only. "
731
+ f"Flip _CLOSE_OLD_VISIBLE_DRYRUN=False after confirming it's ONLY the old pid.")
732
+ continue
733
+ if _kill_headless_pid(target, pid):
734
+ _log(f"CLOSE-OLD-VISIBLE {target}: closed old window pid {pid} (visible recycle; kept fresh window)")
735
+ n += 1
736
+ return n
737
+
738
+
594
739
  def _spawn_rate_ok(target: str):
595
740
  """Anti-spam circuit breaker. Returns (ok, tripped_burst, reason).
596
741
 
@@ -978,6 +1123,9 @@ def _do_recycle_enforce(api_key: str, host_id: str) -> int:
978
1123
  agent (headless_pids, with _kill_headless_pid's reuse-guard) — NEVER blind cmdline. After the
979
1124
  kill it goes stale and the recycle FAST-PATH in _do_respawns relaunches it within seconds ->
980
1125
  SessionStart restores the handoff. Returns number force-killed."""
1126
+ # DEAD-FEATURE (task 222b1b02, Samuel 2026-06-04): RECYCLE disabled in prod — hard no-op
1127
+ # (no recycles are triggered, so there is nothing to enforce). Crash-RESPAWN unaffected.
1128
+ return 0
981
1129
  res = _rpc("mc_recycle_enforce_candidates", {"p_api_key": api_key, "p_host_id": host_id})
982
1130
  if not res or not res.get("ok"):
983
1131
  return 0
@@ -1023,6 +1171,11 @@ def _do_recycle_enforce(api_key: str, host_id: str) -> int:
1023
1171
 
1024
1172
  def _do_recycles(api_key: str, host_id: str) -> int:
1025
1173
  """Uptime-based recycle at task boundary. Returns number recycled."""
1174
+ # DEAD-FEATURE (task 222b1b02, Samuel 2026-06-04): the RECYCLE feature is disabled in
1175
+ # prod (unreliable — kept causing version/env-mismatch storms). Hard no-op in source so
1176
+ # it stays dead even if a schedule row reappears. Crash-RESPAWN (_do_respawns) is
1177
+ # UNAFFECTED — only RECYCLE triggers are killed.
1178
+ return 0
1026
1179
  cfg = _rpc("mc_host_config_get", {"p_api_key": api_key, "p_host_id": host_id})
1027
1180
  if not cfg or not cfg.get("ok"):
1028
1181
  return 0
@@ -1413,6 +1566,10 @@ def _do_version_recycles(api_key: str, host_id: str) -> int:
1413
1566
  mid-task), rate-limited (<=1 version-recycle per agent / 30min), recycle-not-kill (clean handoff via
1414
1567
  mc_request_recycle -> agent exits at its boundary -> _do_respawns relaunches on the new version).
1415
1568
  Recorded via mc_record_recycle (NEVER counts against the mig406 crash respawn cap). Owner-scoped."""
1569
+ # DEAD-FEATURE (task 222b1b02, Samuel 2026-06-04): RECYCLE disabled in prod — hard no-op.
1570
+ # This was the env-mismatch storm source; fixed-at-source via run_agent env-sync, but the
1571
+ # whole recycle feature is being removed per owner. Crash-RESPAWN is unaffected.
1572
+ return 0
1416
1573
  cfg = _rpc("mc_host_config_get", {"p_api_key": api_key, "p_host_id": host_id})
1417
1574
  if not cfg or not cfg.get("ok"):
1418
1575
  return 0
@@ -1437,11 +1594,15 @@ def _do_version_recycles(api_key: str, host_id: str) -> int:
1437
1594
  continue # agent already on the on-disk version (or newer) — nothing to do
1438
1595
  except Exception:
1439
1596
  continue
1440
- if (a.get("status") or "") in BUSY_STATUSES:
1441
- continue # safe-point ONLY never recycle a working agent mid-task
1597
+ if _agent_connected(a):
1598
+ continue # Samuel rule: never version-recycle a CONNECTED agent (live MCP session,
1599
+ # even if idle/standby) — the >3h uptime lifecycle (_do_recycles) is the
1600
+ # only recycle that may touch a connected agent.
1442
1601
  proj, agent = a.get("project_name"), a.get("name")
1443
1602
  if not proj or not agent:
1444
1603
  continue
1604
+ if _recycle_blocked(st, f"{proj}/{agent}", now):
1605
+ continue # 451d33a0 unify: already blocked as a non-converging recycle — don't also version-recycle it
1445
1606
  key = f"verrec/{a.get('project_id')}/{agent}"
1446
1607
  if now - float(st.get(key, 0) or 0) < 1800:
1447
1608
  continue # rate-limit: <=1 version-recycle per agent / 30min (no recycle-storm)
@@ -1731,6 +1731,8 @@ def _build_instructions() -> str:
1731
1731
 
1732
1732
  RULES: MCP tools only. Tasks > messages. Messages <100 tokens (long → task). No empty acks, JSON only. Thread via in_reply_to. sensitive=True for secrets. Sync vs async: for turn-based/shared-state work (chess, lock-step, "who goes first"), use meshcode_call (sync RPC, 30s) — async meshcode_send crosses ~50%/turn between same-model agents and yields-pongs.
1733
1733
 
1734
+ REPLY TO HUMANS VIA THE MESH, NOT THE TERMINAL (product default, Samuel 2026-06-04): when you answer a human (sammybenu/Samuel/ian/fis), you MUST meshcode_send to the mesh. The human watches the DASHBOARD — your Claude Code terminal text and your thinking are INVISIBLE to them, so a terminal-only reply reads as silence ("¿por qué no me contestas?" even while you "answered"). meshcode_send is the ONLY channel they see. To humans write plain prose in THEIR language (Spanish for Samuel), never JSON; JSON-only is for agent↔agent.
1735
+
1734
1736
  CLOSE TASKS IMMEDIATELY (#2 rule, sammybenu 2026-05-22T21:06Z): every time you finish/ship/deliver work for a claimed task, call meshcode_task_complete BEFORE re-entering wait. NO "I'll close it after one more thing." NO leaving claimed tasks lingering in_progress while you do something else — that creates phantom in-progress on the dashboard and the human gets angry. If the work needs human review, set requires_approval=true at task_create OR call meshcode_task_complete (it routes to in_review if reviewer set). The mesh dashboard reflects mc_tasks.status in real-time; stale in_progress = lying to the human.
1735
1737
 
1736
1738
  WORK ASSIGNED TASKS IMMEDIATELY (#3 rule): when meshcode_wait returns pending_tasks or auto_started_task, your NEXT action MUST be to work that task. Do NOT re-enter wait, do NOT ask "what should I do" — the task description tells you. Sequence: read task description → execute the work → meshcode_task_complete → meshcode_wait (which surfaces the next task). One by one until the queue is empty.
@@ -6162,12 +6164,10 @@ def meshcode_recycle_agent(name: str, visible: bool = False) -> Dict[str, Any]:
6162
6164
  visible: True = respawn as a visible focused window; default False =
6163
6165
  preserve the agent's current headless/visible state.
6164
6166
  """
6165
- return be.sb_rpc("mc_recycle_as_agent", {
6166
- "p_api_key": _get_api_key(),
6167
- "p_project_id": _PROJECT_ID,
6168
- "p_agent": name,
6169
- "p_visible": bool(visible),
6170
- })
6167
+ # DEAD-FEATURE (task 222b1b02, Samuel 2026-06-04): recycle is disabled in prod
6168
+ # (unreliable). No-op — do not call the RPC.
6169
+ return {"ok": False, "error_code": "recycle_disabled",
6170
+ "error": "recycle is disabled (feature removed — was unreliable). No action taken."}
6171
6171
 
6172
6172
 
6173
6173
  @mcp.tool()
@@ -6185,12 +6185,9 @@ def meshcode_recycle_fleet(visible: bool = False) -> Dict[str, Any]:
6185
6185
  visible: True = respawn each as a visible focused window; default False
6186
6186
  = preserve each agent's current headless/visible state.
6187
6187
  """
6188
- return be.sb_rpc("mc_recycle_as_agent", {
6189
- "p_api_key": _get_api_key(),
6190
- "p_project_id": _PROJECT_ID,
6191
- "p_agent": None, # NULL = whole fleet (all running agents)
6192
- "p_visible": bool(visible),
6193
- })
6188
+ # DEAD-FEATURE (task 222b1b02, Samuel 2026-06-04): recycle is disabled in prod. No-op.
6189
+ return {"ok": False, "error_code": "recycle_disabled",
6190
+ "error": "recycle is disabled (feature removed — was unreliable). No action taken."}
6194
6191
 
6195
6192
 
6196
6193
  @mcp.tool()
@@ -6217,12 +6214,10 @@ def meshcode_set_recycle_schedule(interval_hours: int = 6, enabled: bool = True,
6217
6214
  "error_code": "not_yet_supported",
6218
6215
  "error": "per-agent recycle schedule is a fast-follow; pass agent=None for the mesh-global schedule.",
6219
6216
  }
6220
- return be.sb_rpc("mc_set_recycle_schedule_as_agent", {
6221
- "p_api_key": _get_api_key(),
6222
- "p_project_id": _PROJECT_ID,
6223
- "p_enabled": bool(enabled),
6224
- "p_interval_hours": int(interval_hours),
6225
- })
6217
+ # DEAD-FEATURE (task 222b1b02, Samuel 2026-06-04): auto-recycle scheduling is disabled
6218
+ # in prod. No-op — never (re)enable a schedule.
6219
+ return {"ok": False, "error_code": "recycle_disabled",
6220
+ "error": "auto-recycle scheduling is disabled (feature removed — was unreliable). No action taken."}
6226
6221
 
6227
6222
 
6228
6223
  @mcp.tool()
@@ -17,15 +17,125 @@ from __future__ import annotations
17
17
  import json
18
18
  import os
19
19
  import platform
20
+ import re
20
21
  import shlex
21
22
  import shutil
22
23
  import subprocess
23
24
  import sys
25
+ import time
24
26
  import urllib.parse
25
27
  from pathlib import Path
26
28
  from typing import Iterable, Optional
27
29
 
28
30
 
31
+ # SECURITY (audit P1-4 RCE/DoS): agent names get interpolated into the platform
32
+ # shell / cmd.exe launch string below. shlex.quote is POSIX-only and does NOT
33
+ # neutralize cmd.exe metacharacters, so a name like `x" & calc & "` would break
34
+ # out of the quoting on Windows (cmd /k) and execute arbitrary code. Real agent
35
+ # names are always plain identifiers — hard-reject anything else at the boundary.
36
+ # Hardened allowlist (chief@mesh-dev spec):
37
+ # - first char [A-Za-z0-9_] -> NO leading dash, so `-rf` / `--version` can't be
38
+ # read as a flag/arg if the name ever reaches a bare positional (arg-injection)
39
+ # - then [A-Za-z0-9_-]{0,63} -> total length 1..64, capping the 300-char-name DoS
40
+ # - anchor with \Z, not $ -> in Python `$` also matches just before a trailing
41
+ # newline, so `name\n` would slip through `^...$`. \Z = absolute end of string.
42
+ _VALID_AGENT_NAME = re.compile(r"^[A-Za-z0-9_][A-Za-z0-9_-]{0,63}\Z")
43
+
44
+ # Launch-storm control (audit P1: each "Launch All" click spawns a terminal per
45
+ # agent UNCONDITIONALLY; repeat clicks storm the desktop with dup terminals).
46
+ # Two dedup layers + a hard cap:
47
+ # (1) heartbeat liveness — skip agents already running (best-effort, network)
48
+ # (2) local cooldown file — per-agent last-spawn ts; covers the spawn->first-
49
+ # heartbeat gap where (1) is blind, and absorbs rapid repeat-clicks. No
50
+ # network, so it is the always-on backstop.
51
+ _MAX_BATCH = 32 # hard cap: agents spawned / call
52
+ _LAUNCH_COOLDOWN_S = 30 # min seconds between same-agent spawns
53
+ _LIVE_HEARTBEAT_S = 20 # heartbeat age < this = agent live
54
+ _COOLDOWN_FILE = Path.home() / ".meshcode" / "launch_cooldown.json"
55
+
56
+
57
+ def is_valid_agent_name(name: str) -> bool:
58
+ """Shared allowlist gate — True iff `name` is a safe agent identifier.
59
+
60
+ Single source of truth for the launch path AND `meshcode run` (run_agent),
61
+ so the RCE/DoS boundary can't drift between callers.
62
+ """
63
+ return bool(name) and bool(_VALID_AGENT_NAME.match(name))
64
+
65
+
66
+ def _read_cooldowns() -> dict:
67
+ try:
68
+ return json.loads(_COOLDOWN_FILE.read_text(encoding="utf-8")) or {}
69
+ except Exception:
70
+ return {}
71
+
72
+
73
+ def _record_spawn(name: str, now: Optional[float] = None) -> None:
74
+ """Stamp `name`'s last-spawn ts in the cooldown file (best-effort, atomic)."""
75
+ try:
76
+ data = _read_cooldowns()
77
+ data[name] = now if now is not None else time.time()
78
+ _COOLDOWN_FILE.parent.mkdir(parents=True, exist_ok=True)
79
+ tmp = _COOLDOWN_FILE.with_suffix(".json.tmp")
80
+ tmp.write_text(json.dumps(data), encoding="utf-8")
81
+ tmp.replace(_COOLDOWN_FILE)
82
+ except Exception:
83
+ pass
84
+
85
+
86
+ def _in_cooldown(name: str, cooldowns: dict, now: float) -> bool:
87
+ try:
88
+ return (now - float(cooldowns.get(name, 0))) < _LAUNCH_COOLDOWN_S
89
+ except (TypeError, ValueError):
90
+ return False
91
+
92
+
93
+ def live_agent_names(names: Iterable[str], project: Optional[str] = None) -> set:
94
+ """Best-effort set of `names` whose mc_agents heartbeat is fresh (<20s).
95
+
96
+ Shared liveness primitive for the dedup lane (co-owned w/ chief@mesh-dev).
97
+ Network + creds required; ANY failure (offline, no api_key, project
98
+ unresolved, RPC error) returns an EMPTY set so a legitimate launch is never
99
+ blocked — the cooldown file is the always-on backstop. NB: the meshcode://
100
+ launch URL carries no project today, so resolution falls back to
101
+ MESHCODE_PROJECT env; pass `project` explicitly for reliable liveness.
102
+ """
103
+ wanted = {n for n in names if is_valid_agent_name(n)}
104
+ if not wanted:
105
+ return set()
106
+ try:
107
+ import importlib
108
+ cv = importlib.import_module("meshcode.comms_v4")
109
+ api_key = cv._load_api_key_for_cli()
110
+ proj = project or os.environ.get("MESHCODE_PROJECT")
111
+ if not api_key or not proj:
112
+ return set()
113
+ pid = cv.get_project_id(proj)
114
+ if not pid:
115
+ return set()
116
+ r = cv.sb_rpc("mc_get_agents", {"p_api_key": api_key, "p_project_id": pid,
117
+ "p_agent_name": None, "p_select": None,
118
+ "p_limit": None})
119
+ agents = (r or {}).get("agents") or []
120
+ from datetime import datetime, timezone
121
+ now = datetime.now(timezone.utc)
122
+ live = set()
123
+ for a in agents:
124
+ nm = a.get("name")
125
+ ts = a.get("last_heartbeat")
126
+ if nm not in wanted or not ts:
127
+ continue
128
+ try:
129
+ dt = datetime.fromisoformat(str(ts).replace("Z", "+00:00"))
130
+ if (now - dt).total_seconds() < _LIVE_HEARTBEAT_S:
131
+ live.add(nm)
132
+ except Exception:
133
+ continue
134
+ return live
135
+ except Exception:
136
+ return set()
137
+
138
+
29
139
  # ============================================================
30
140
  # Per-OS terminal spawn
31
141
  # ============================================================
@@ -165,7 +275,37 @@ def cmd_launch_batch(agent_names: Iterable[str]) -> int:
165
275
  # Resolve `meshcode` binary path (CLI wrapper installed by pip).
166
276
  mc_bin = shutil.which("meshcode") or "meshcode"
167
277
 
278
+ # RATE LIMIT: hard-cap the batch so a crafted `agents=` list can't spawn an
279
+ # unbounded number of terminals (DoS). Excess is reported, never launched.
280
+ if len(names) > _MAX_BATCH:
281
+ for name in names[_MAX_BATCH:]:
282
+ skipped.append({"agent": name, "reason": f"batch cap {_MAX_BATCH} exceeded"})
283
+ names = names[:_MAX_BATCH]
284
+
285
+ # DEDUP inputs computed ONCE for the whole batch: live set (heartbeat) +
286
+ # cooldown snapshot. `now` is shared so all spawns in this call stamp the
287
+ # same instant.
288
+ live = live_agent_names(names)
289
+ cooldowns = _read_cooldowns()
290
+ now = time.time()
291
+
168
292
  for name in names:
293
+ # SECURITY (audit P1-4 RCE/DoS): reject any name that isn't a plain agent
294
+ # identifier BEFORE it reaches the shell/cmd.exe launch string below.
295
+ if not is_valid_agent_name(name):
296
+ skipped.append({"agent": name,
297
+ "reason": "invalid agent name (allowlist ^[A-Za-z0-9_][A-Za-z0-9_-]{0,63}$)"})
298
+ continue
299
+ # DEDUP 1: agent already running (fresh heartbeat) -> don't double-spawn.
300
+ if name in live:
301
+ skipped.append({"agent": name, "reason": "already live (heartbeat <20s)"})
302
+ continue
303
+ # DEDUP 2: spawned within the cooldown window -> absorbs repeat-clicks and
304
+ # the spawn->first-heartbeat gap that layer 1 cannot see.
305
+ if _in_cooldown(name, cooldowns, now):
306
+ skipped.append({"agent": name,
307
+ "reason": f"cooldown {_LAUNCH_COOLDOWN_S}s (recently launched)"})
308
+ continue
169
309
  # PER-PLATFORM quoting (mesh-core FIX2): cmd.exe wants double-quotes, not POSIX shlex
170
310
  # single-quotes (cmd.exe passes single-quotes through literally -> file-not-found).
171
311
  if sys.platform == "win32":
@@ -175,6 +315,7 @@ def cmd_launch_batch(agent_names: Iterable[str]) -> int:
175
315
  ok, info = _spawn_terminal(cmd)
176
316
  if ok:
177
317
  launched.append(name)
318
+ _record_spawn(name, now)
178
319
  else:
179
320
  skipped.append({"agent": name, "reason": info})
180
321
 
@@ -695,6 +695,21 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
695
695
  boot path flips mc_agents.autonomous_mode=true at register time.
696
696
  Task f248d98e Phase 1 (no-DDL subset).
697
697
  """
698
+ # SECURITY (audit P1-4): the agent name flows into workspace path lookups and
699
+ # a child editor/`claude` launch. Reject anything that isn't a plain agent
700
+ # identifier (esp. a leading dash, which a child tool could read as a flag) —
701
+ # belt-and-suspenders with the launch-batch allowlist (shared source of truth).
702
+ try:
703
+ from meshcode.protocol_handler import is_valid_agent_name as _ok_name
704
+ except Exception:
705
+ import re as _re
706
+ _ok_name = lambda n: bool(n) and bool(
707
+ _re.match(r"^[A-Za-z0-9_][A-Za-z0-9_-]{0,63}\Z", str(n)))
708
+ if not _ok_name(agent):
709
+ print(json.dumps({"ok": False, "error": f"invalid agent name: {agent!r}",
710
+ "error_code": "invalid_agent_name"}))
711
+ return 1
712
+
698
713
  # Propagate autonomous flag to the editor + MCP child via env var.
699
714
  if autonomous:
700
715
  os.environ["MESHCODE_AUTONOMOUS"] = "1"
@@ -805,6 +820,22 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
805
820
  print(f"[meshcode] Run `meshcode setup {resolved_project} {agent}` to fix.", file=sys.stderr)
806
821
  return 2
807
822
 
823
+ # Env-mismatch storm fix (2.11.109): the non-blocking auto-pip above updated the
824
+ # LAUNCHER env (sys.executable). But THIS agent's MCP server runs from this
825
+ # workspace's .mcp.json `command` python — that's the env that reports
826
+ # cli_version. Sync IT to the launcher's installed version so hostd
827
+ # version-recycle CONVERGES instead of looping forever. Non-blocking, best-effort.
828
+ if not dry_run:
829
+ try:
830
+ _doc = json.loads(mcp_json_path.read_text(encoding="utf-8"))
831
+ for _srv in (_doc.get("mcpServers") or {}).values():
832
+ _cmd = _srv.get("command")
833
+ if _cmd:
834
+ self_update.sync_agent_env(_cmd)
835
+ break
836
+ except Exception:
837
+ pass
838
+
808
839
  # ── Validate stop hook exists (required for wait-loop integrity) ─
809
840
  # If the workspace was created on an old CLI that didn't install hooks
810
841
  # (or someone wiped .claude/), Claude Code has nothing blocking turn-end
@@ -275,6 +275,7 @@ state_dir.mkdir(parents=True, exist_ok=True)
275
275
  mode = sys.argv[1] if len(sys.argv) > 1 else "pip"
276
276
  target_version = sys.argv[2] if len(sys.argv) > 2 else None
277
277
  site_flag = sys.argv[3] if len(sys.argv) > 3 else "system"
278
+ target_python = sys.argv[4] if len(sys.argv) > 4 and sys.argv[4] else None # agent MCP-server env
278
279
 
279
280
  try:
280
281
  if mode == "pipx":
@@ -282,11 +283,12 @@ try:
282
283
  else:
283
284
  # --no-cache-dir (task 14782bb4 / urgent): never let pip serve a STALE cached wheel — always
284
285
  # fetch the true latest from PyPI (auto-update was grabbing an old cached version otherwise).
285
- cmd = [sys.executable, "-m", "pip", "install", "-U", "--no-cache-dir",
286
+ exe = target_python or sys.executable
287
+ cmd = [exe, "-m", "pip", "install", "-U", "--no-cache-dir",
286
288
  "--disable-pip-version-check", "--quiet"]
287
- # Keep upgrade in the same site as the current install so the
288
- # user-PATH shim (~/Library/Python/X.Y/bin) doesn't get orphaned.
289
- if site_flag == "user":
289
+ # --user only for the LAUNCHER's own user-site install; an explicit
290
+ # target_python (the agent's MCP env / venv) installs into ITS env.
291
+ if site_flag == "user" and not target_python:
290
292
  cmd.append("--user")
291
293
  cmd.append("meshcode")
292
294
  with open(log_path, "ab") as logf:
@@ -315,12 +317,13 @@ finally:
315
317
  '''
316
318
 
317
319
 
318
- def _spawn_background_updater(target_version: str) -> bool:
320
+ def _spawn_background_updater(target_version: str, target_python: Optional[str] = None) -> bool:
319
321
  """Spawn a fully detached subprocess that runs the updater.
320
322
 
321
323
  The parent (current `meshcode run`) returns immediately. The child
322
324
  runs pip install in the background, writes the result to disk, and
323
- exits. Next `meshcode run` consumes the result.
325
+ exits. Next `meshcode run` consumes the result. `target_python` (when set)
326
+ is the agent's MCP-server env — pip installs into IT, not the launcher.
324
327
  """
325
328
  if not _acquire_lock():
326
329
  return False
@@ -331,7 +334,7 @@ def _spawn_background_updater(target_version: str) -> bool:
331
334
  # We pass the runner code via stdin so we don't need to ship a
332
335
  # second .py file. The child reads it from sys.stdin and execs it.
333
336
  runner = f"import sys; exec(sys.stdin.read())"
334
- args = [sys.executable, "-c", runner, mode, target_version, site_flag]
337
+ args = [sys.executable, "-c", runner, mode, target_version, site_flag, target_python or ""]
335
338
 
336
339
  try:
337
340
  if sys.platform == "win32":
@@ -418,6 +421,46 @@ def check_and_maybe_update(verbose: bool = False) -> None:
418
421
  print(f"[meshcode] downloading {latest} in background...", file=sys.stderr)
419
422
 
420
423
 
424
+ def _env_version(python_exe: str) -> Optional[str]:
425
+ """meshcode.__version__ as seen by ANOTHER python env (the agent's MCP server)."""
426
+ try:
427
+ out = subprocess.run(
428
+ [python_exe, "-c", "import meshcode,sys; sys.stdout.write(meshcode.__version__)"],
429
+ capture_output=True, text=True, timeout=10).stdout.strip()
430
+ return out or None
431
+ except Exception:
432
+ return None
433
+
434
+
435
+ def sync_agent_env(mcp_python: str, verbose: bool = False) -> None:
436
+ """Bring the agent's MCP-SERVER env (mcp_python, from the workspace .mcp.json
437
+ `command`) up to the LAUNCHER's installed meshcode version.
438
+
439
+ The MCP server is the env that reports cli_version. If it lags hostd's on-disk
440
+ version, hostd version-recycles the agent FOREVER (the env-mismatch storm),
441
+ because run_agent's normal auto-pip only updates the LAUNCHER env
442
+ (sys.executable). This syncs the env that actually matters. Non-blocking
443
+ (background pip). No-op if same env / already current / opted out / unreadable.
444
+ """
445
+ try:
446
+ if not mcp_python:
447
+ return
448
+ if os.path.realpath(mcp_python) == os.path.realpath(sys.executable):
449
+ return # same env — the normal launcher update already covers it
450
+ if update_disabled():
451
+ return
452
+ launcher_ver = _current_version()
453
+ env_ver = _env_version(mcp_python)
454
+ if not launcher_ver or not env_ver:
455
+ return
456
+ if _is_newer(launcher_ver, env_ver):
457
+ if _spawn_background_updater(launcher_ver, mcp_python) and verbose:
458
+ print(f"[meshcode] syncing agent env {env_ver} -> {launcher_ver} in background...",
459
+ file=sys.stderr)
460
+ except Exception:
461
+ pass
462
+
463
+
421
464
  # ============================================================
422
465
  # Blocking variant — used by `meshcode run` to guarantee the editor
423
466
  # subprocess inherits the latest meshcode_mcp/server.py on disk.
@@ -1333,6 +1333,11 @@ If `meshcode_wait()` times out, call it again with a 2× longer timeout (cap 180
1333
1333
  - Tasks > messages. Use `meshcode_task_create / task_claim / task_complete`
1334
1334
  for trackable work. Keep messages <100 tokens (signals only).
1335
1335
  - No empty acks. JSON reports only.
1336
+ - **Reply to humans via the mesh, NOT the terminal** (product default): when you
1337
+ answer a human (Samuel/sammybenu/ian/fis), `meshcode_send` to the mesh. The
1338
+ human watches the DASHBOARD — your Claude Code terminal text + thinking are
1339
+ invisible to them, so a terminal-only reply reads as silence. Write plain prose
1340
+ in their language (Spanish for Samuel), never JSON — JSON is for agent↔agent.
1336
1341
  - Threading: pass `in_reply_to`.
1337
1342
  - Sync vs async: for turn-based or shared-state coordination (chess, single-writer
1338
1343
  doc, lock-step handoffs, "who goes first" decisions), prefer
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.11.107
3
+ Version: 2.11.109
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT