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.
- {meshcode-2.11.8 → meshcode-2.11.10}/PKG-INFO +1 -1
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/__init__.py +1 -1
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/comms_v4.py +58 -4
- meshcode-2.11.10/meshcode/doctor.py +277 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/meshcode_mcp/server.py +80 -32
- meshcode-2.11.10/meshcode/meshcode_mcp/test_install_guard.py +75 -0
- meshcode-2.11.10/meshcode/meshcode_mcp/test_prefs_claude_version.py +82 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/run_agent.py +62 -8
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode.egg-info/SOURCES.txt +4 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/pyproject.toml +1 -1
- meshcode-2.11.10/tests/test_doctor.py +123 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_install_guard.py +68 -10
- {meshcode-2.11.8 → meshcode-2.11.10}/README.md +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/_stop_hook_template.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/ascii_art.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/cli.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/compat.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/daemon.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/error_hints.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/exceptions.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/invites.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/launcher.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/launcher_install.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/preferences.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/protocol_handler.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/quickstart.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/scripts/check_secrets.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/scripts/race_rate_harness.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/secrets.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/self_update.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/setup_clients.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/supervisor.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode/upload.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/comms_v4.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/__init__.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/ascii_art.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/cli.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/comms_v4.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/compat.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/error_hints.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/exceptions.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/invites.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/launcher.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/launcher_install.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/meshcode_mcp/server.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/preferences.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/protocol_v2.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/quickstart.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/run_agent.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/secrets.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/self_update.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/setup_clients.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/supervisor.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/meshcode/upload.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/scripts/sentinel.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/tests/test_core.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/tests/test_exceptions.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/tests/test_migration_integrity.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/tests/test_security_regressions.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/tests/test_sentinel.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-backend-wt/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/__init__.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/ascii_art.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/cli.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/comms_v4.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/compat.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/error_hints.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/exceptions.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/invites.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/launcher.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/launcher_install.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/server.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/preferences.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/protocol_v2.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/quickstart.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/run_agent.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/secrets.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/self_update.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/setup_clients.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/supervisor.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/build/lib/meshcode/upload.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/comms_v4.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/__init__.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/ascii_art.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/cli.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/comms_v4.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/compat.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/error_hints.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/exceptions.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/invites.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/launcher.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/launcher_install.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/meshcode_mcp/server.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/preferences.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/protocol_v2.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/quickstart.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/run_agent.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/secrets.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/self_update.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/setup_clients.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/supervisor.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/meshcode/upload.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/scripts/sentinel.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/tests/test_core.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/tests/test_exceptions.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/tests/test_migration_integrity.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/tests/test_security_regressions.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/tests/test_sentinel.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-noun-wt/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/comms_v4.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/__init__.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/ascii_art.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/cli.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/comms_v4.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/compat.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/error_hints.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/exceptions.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/invites.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/launcher.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/launcher_install.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/meshcode_mcp/server.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/preferences.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/protocol_v2.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/quickstart.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/run_agent.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/secrets.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/self_update.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/setup_clients.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/supervisor.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/meshcode/upload.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/scripts/sentinel.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/tests/test_core.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/tests/test_exceptions.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/tests/test_migration_integrity.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/tests/test_security_regressions.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/tests/test_sentinel.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode-tasks-wt/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/setup.cfg +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_auto_update_hardening.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_boot_bug_regression.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_core.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_epistemic_v1_python_sdk.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_epistemic_v1_stop_conditions.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_exceptions.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_lease_sigterm_release.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_migration_integrity.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_security_regressions.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_sentinel.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_sleep_signals.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_stay_on_loop_hook.py +0 -0
- {meshcode-2.11.8 → meshcode-2.11.10}/tests/test_wait_open_tasks_contradiction.py +0 -0
|
@@ -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
|
-
#
|
|
2749
|
-
|
|
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"
|
|
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
|
|
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:
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
Soft-fail throughout
|
|
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
|
-
|
|
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
|
|
1732
|
-
f"(mtime {
|
|
1733
|
-
f"
|
|
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
|
|
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"
|