swarph-cli 0.48.2__tar.gz → 0.49.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (340) hide show
  1. {swarph_cli-0.48.2/src/swarph_cli.egg-info → swarph_cli-0.49.0}/PKG-INFO +1 -1
  2. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/pyproject.toml +1 -1
  3. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/__init__.py +1 -1
  4. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/brain_ask.py +27 -11
  5. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/cell_selfcheck.py +10 -4
  6. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/codegraph.py +2 -2
  7. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/codegraph_hook.py +10 -2
  8. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/daemon.py +6 -1
  9. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/event.py +5 -2
  10. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/init.py +6 -3
  11. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/mesh.py +71 -15
  12. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/monitor.py +3 -1
  13. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/onboard.py +6 -2
  14. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/ratify.py +5 -1
  15. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/watchdog.py +39 -14
  16. swarph_cli-0.49.0/src/swarph_cli/gateway_default.py +70 -0
  17. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/guide/GUIDE.md +1 -1
  18. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/systemd/codex-waker.default +6 -2
  19. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/systemd/peer-reply-drain.default +6 -2
  20. {swarph_cli-0.48.2 → swarph_cli-0.49.0/src/swarph_cli.egg-info}/PKG-INFO +1 -1
  21. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli.egg-info/SOURCES.txt +3 -0
  22. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_546_default_gateway.py +22 -32
  23. swarph_cli-0.49.0/tests/test_578_no_machine_specific_host_defaults.py +458 -0
  24. swarph_cli-0.49.0/tests/test_579_register_bootstrap_message.py +98 -0
  25. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_brain_ask_command.py +30 -5
  26. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_content_composition.py +4 -0
  27. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_daemon_command.py +2 -0
  28. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_mesh_command.py +9 -2
  29. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_onboard_command.py +4 -0
  30. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_ratify_command.py +5 -0
  31. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_shipped_units_match_the_cli.py +40 -8
  32. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_watchdog.py +8 -4
  33. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/LICENSE +0 -0
  34. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/README.md +0 -0
  35. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/setup.cfg +0 -0
  36. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/__main__.py +0 -0
  37. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/bench/__init__.py +0 -0
  38. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/bench/backends.py +0 -0
  39. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/bench/pack.py +0 -0
  40. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/bench/prices.py +0 -0
  41. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/bench/quality.py +0 -0
  42. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/bench/refresh_prices.py +0 -0
  43. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/bench/runner.py +0 -0
  44. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/bench/validate.py +0 -0
  45. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/caller.py +0 -0
  46. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/capture/__init__.py +0 -0
  47. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/capture/harden.py +0 -0
  48. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/capture/lineage.py +0 -0
  49. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/capture/liveness.py +0 -0
  50. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/capture/manifest.py +0 -0
  51. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/capture/paths.py +0 -0
  52. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/capture/verify.py +0 -0
  53. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/cell.py +0 -0
  54. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/chain_token.py +0 -0
  55. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/__init__.py +0 -0
  56. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/_content.py +0 -0
  57. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/_display.py +0 -0
  58. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/_gateway_client.py +0 -0
  59. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/add.py +0 -0
  60. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/bench.py +0 -0
  61. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/board.py +0 -0
  62. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/board_merge_check.py +0 -0
  63. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/brain.py +0 -0
  64. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/cell.py +0 -0
  65. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/channel.py +0 -0
  66. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/chat.py +0 -0
  67. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/codex_hook_output.py +0 -0
  68. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/codex_waker.py +0 -0
  69. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/compress.py +0 -0
  70. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/gateway.py +0 -0
  71. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/gh_route.py +0 -0
  72. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/group.py +0 -0
  73. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/guide.py +0 -0
  74. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/highlight.py +0 -0
  75. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/hook_output.py +0 -0
  76. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/hooks.py +0 -0
  77. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/import_session.py +0 -0
  78. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/install_codex_hooks.py +0 -0
  79. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/install_hook.py +0 -0
  80. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/install_multiplexer.py +0 -0
  81. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/install_postcompact_hook.py +0 -0
  82. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/install_wake_hook.py +0 -0
  83. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/lane.py +0 -0
  84. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/mcp_server.py +0 -0
  85. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/memory.py +0 -0
  86. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/memory_emit_hook.py +0 -0
  87. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/memory_sync.py +0 -0
  88. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/okf_links.py +0 -0
  89. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/peer_reply_drain.py +0 -0
  90. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/postcompact_hook_output.py +0 -0
  91. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/protocol_handler.py +0 -0
  92. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/schedule.py +0 -0
  93. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/security.py +0 -0
  94. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/service.py +0 -0
  95. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/spawn.py +0 -0
  96. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/timeline.py +0 -0
  97. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/commands/wake_hook_output.py +0 -0
  98. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/compress/__init__.py +0 -0
  99. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/compress/levers.py +0 -0
  100. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/compress/marker.py +0 -0
  101. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/compress/verify.py +0 -0
  102. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/console_safe.py +0 -0
  103. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/delivery_queue.py +0 -0
  104. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/gateway/__init__.py +0 -0
  105. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/gateway/feature_registry.py +0 -0
  106. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/gateway/lanes_control.py +0 -0
  107. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/gateway/schema.sql +0 -0
  108. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/gateway/server.py +0 -0
  109. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/gateway/services_control.py +0 -0
  110. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/gh_identity.py +0 -0
  111. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/main.py +0 -0
  112. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/multiplexer.py +0 -0
  113. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/parsers/__init__.py +0 -0
  114. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/parsers/claude.py +0 -0
  115. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/payloads/postcompact/_shim.cmd +0 -0
  116. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/payloads/postcompact/postcompact-recall-grok.sh +0 -0
  117. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/payloads/postcompact/postcompact-recall.sh +0 -0
  118. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/payloads/postcompact/posttooluse-emit-antigravity.sh +0 -0
  119. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/payloads/postcompact/posttooluse-emit-claude.sh +0 -0
  120. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/payloads/postcompact/posttooluse-emit-grok.sh +0 -0
  121. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/payloads/postcompact/posttooluse-emit.sh +0 -0
  122. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/payloads/postcompact/posttooluse-recall.sh +0 -0
  123. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/payloads/postcompact/precompact-flag.sh +0 -0
  124. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/payloads/postcompact/preinvocation-recall-antigravity.sh +0 -0
  125. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/payloads/postcompact/sessionstart-recall.sh +0 -0
  126. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/peer_executor.py +0 -0
  127. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/peer_executor_windows.py +0 -0
  128. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/peer_reconciliation.py +0 -0
  129. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/peer_reply_outbox.py +0 -0
  130. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/peer_service_host.py +0 -0
  131. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/peer_service_payload.py +0 -0
  132. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/peer_service_reply.py +0 -0
  133. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/peer_staging.py +0 -0
  134. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/scripts/__init__.py +0 -0
  135. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/scripts/dm_notify_filter.py +0 -0
  136. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/scripts/ensure_monitor.sh +0 -0
  137. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/scripts/install_codex_waker_windows.ps1 +0 -0
  138. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/service/__init__.py +0 -0
  139. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/service/app.py +0 -0
  140. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/service/providers.py +0 -0
  141. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/session_bridge.py +0 -0
  142. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/stall_alert.py +0 -0
  143. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/systemd/swarph-codex-outbox@.service +0 -0
  144. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/systemd/swarph-codex-outbox@.timer +0 -0
  145. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/systemd/swarph-codex-waker@.service +0 -0
  146. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/systemd/swarph-codex-waker@.timer +0 -0
  147. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/systemd/swarph-peer-reply-drain@.service +0 -0
  148. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/systemd/swarph-peer-reply-drain@.timer +0 -0
  149. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
  150. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
  151. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
  152. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli/tokens.py +0 -0
  153. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
  154. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli.egg-info/entry_points.txt +0 -0
  155. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli.egg-info/requires.txt +0 -0
  156. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/src/swarph_cli.egg-info/top_level.txt +0 -0
  157. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_124_register_merge.py +0 -0
  158. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_201_win32_bash_refusal.py +0 -0
  159. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_243_onboard_per_peer_token.py +0 -0
  160. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_307_task4_ask_reply.py +0 -0
  161. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_319_refusal_exit_codes.py +0 -0
  162. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_323_mistral_backend.py +0 -0
  163. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_397_gh_identity_router.py +0 -0
  164. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_397_router_preflight.py +0 -0
  165. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_467b_onboard_operator_credential.py +0 -0
  166. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_523_guide.py +0 -0
  167. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_524_conftest_hermeticity.py +0 -0
  168. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_532_accept_flag.py +0 -0
  169. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_535_version_capability.py +0 -0
  170. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_548_gateway_remainder.py +0 -0
  171. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_552_codegraph_gateway.py +0 -0
  172. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_555_relay_row_shape.py +0 -0
  173. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_556_relay_freshness.py +0 -0
  174. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_84_cli_labels.py +0 -0
  175. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_add_security_gate.py +0 -0
  176. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_artifact_add.py +0 -0
  177. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_artifact_hash.py +0 -0
  178. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_artifact_lib.py +0 -0
  179. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_artifact_mcp.py +0 -0
  180. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_artifact_skill.py +0 -0
  181. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_artifact_tool.py +0 -0
  182. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_artifact_uri.py +0 -0
  183. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_bench_backends.py +0 -0
  184. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_bench_command.py +0 -0
  185. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_bench_pack.py +0 -0
  186. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_bench_prices.py +0 -0
  187. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_bench_quality.py +0 -0
  188. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_bench_runner.py +0 -0
  189. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_bench_validate.py +0 -0
  190. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_board_card_thread.py +0 -0
  191. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_board_command.py +0 -0
  192. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_board_merge_check.py +0 -0
  193. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_board_obligation_close.py +0 -0
  194. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_brain_ask_env_diagnosis.py +0 -0
  195. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_brain_ask_identity_explicitness.py +0 -0
  196. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_brain_command.py +0 -0
  197. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_caller_meta_guard.py +0 -0
  198. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_capture_lineage.py +0 -0
  199. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_capture_liveness.py +0 -0
  200. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_capture_manifest.py +0 -0
  201. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_capture_paths.py +0 -0
  202. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_capture_security.py +0 -0
  203. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_cell_command_dispatch.py +0 -0
  204. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_cell_harden.py +0 -0
  205. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_cell_loader.py +0 -0
  206. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_cell_selfcheck.py +0 -0
  207. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_cell_verify.py +0 -0
  208. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_chain_token.py +0 -0
  209. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_channel_command.py +0 -0
  210. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_channel_poll_never_breaks_dms.py +0 -0
  211. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_channel_post.py +0 -0
  212. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_channel_wake_policy_filter.py +0 -0
  213. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_chat_command.py +0 -0
  214. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_claude_parser.py +0 -0
  215. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_claude_tmux_template.py +0 -0
  216. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_codegraph_caller_scope.py +0 -0
  217. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_codegraph_command.py +0 -0
  218. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_codegraph_hook.py +0 -0
  219. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_codegraph_mcp.py +0 -0
  220. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_codex_waker.py +0 -0
  221. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_compress_caller_convention.py +0 -0
  222. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_compress_command.py +0 -0
  223. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_compress_levers.py +0 -0
  224. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_compress_marker.py +0 -0
  225. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_compress_verify.py +0 -0
  226. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_console_encoding_e2e.py +0 -0
  227. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_cursor_membrane.py +0 -0
  228. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_daemon_console_encoding.py +0 -0
  229. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_daemon_delivery.py +0 -0
  230. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_delivery_queue.py +0 -0
  231. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_display_sanitize.py +0 -0
  232. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_ensure_monitor_no_peer_default.py +0 -0
  233. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_event_emit.py +0 -0
  234. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_feature_to_uri.py +0 -0
  235. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_gateway_brain.py +0 -0
  236. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_gateway_client.py +0 -0
  237. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_gateway_command.py +0 -0
  238. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_gateway_message_idempotency.py +0 -0
  239. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_gh_route_doctor_mesh_name.py +0 -0
  240. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_group_command.py +0 -0
  241. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_heartbeat_check.py +0 -0
  242. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_highlight_command.py +0 -0
  243. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_hook_output.py +0 -0
  244. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_hooks_activity_marker.py +0 -0
  245. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_hooks_add.py +0 -0
  246. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_hooks_bundle.py +0 -0
  247. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_hooks_init.py +0 -0
  248. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_hooks_legacy_generation_removal.py +0 -0
  249. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_hooks_lifecycle.py +0 -0
  250. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_hooks_merge.py +0 -0
  251. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_hooks_verify.py +0 -0
  252. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_hooks_win32_bash_wrapper.py +0 -0
  253. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_hooks_win32_command_path.py +0 -0
  254. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_import_command.py +0 -0
  255. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_inbox_identity_provenance.py +0 -0
  256. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_init_command.py +0 -0
  257. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_init_provider_menu.py +0 -0
  258. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_install_codex_hooks.py +0 -0
  259. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_install_hook.py +0 -0
  260. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_install_multiplexer_command.py +0 -0
  261. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_install_postcompact_hook.py +0 -0
  262. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_install_wake_hook.py +0 -0
  263. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_lane_command.py +0 -0
  264. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_main.py +0 -0
  265. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_main_unknown_verb_guard.py +0 -0
  266. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_mcp_server.py +0 -0
  267. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_memory_command.py +0 -0
  268. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_memory_gateway.py +0 -0
  269. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_memory_sync.py +0 -0
  270. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_memory_sync_membrane.py +0 -0
  271. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_mesh_inbox_mark_read.py +0 -0
  272. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_mesh_inbox_sanitize.py +0 -0
  273. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_mesh_send_unknown_peer.py +0 -0
  274. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_mesh_sidecar.py +0 -0
  275. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_mesh_sidecar_cursor_decoupled.py +0 -0
  276. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_mesh_sidecar_wake_survives_read.py +0 -0
  277. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_meta_edge_identity.py +0 -0
  278. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_monitor_channel_poll.py +0 -0
  279. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_monitor_cli.py +0 -0
  280. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_monitor_daemon_collision.py +0 -0
  281. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_monitor_self_name_verification.py +0 -0
  282. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_monitor_state_split.py +0 -0
  283. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_multiplexer.py +0 -0
  284. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_muse_membrane.py +0 -0
  285. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_muse_own_cli.py +0 -0
  286. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_obligation_close_is_surfaced.py +0 -0
  287. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_okf_links.py +0 -0
  288. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_packaged_artifacts_exist.py +0 -0
  289. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_peer_executor.py +0 -0
  290. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_peer_executor_windows.py +0 -0
  291. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_peer_reply_drain_command.py +0 -0
  292. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_peer_reply_drain_supervisor.py +0 -0
  293. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_peer_reply_outbox.py +0 -0
  294. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_peer_reply_transport.py +0 -0
  295. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_peer_service_e2e.py +0 -0
  296. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_peer_service_host.py +0 -0
  297. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_peer_service_payload.py +0 -0
  298. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_peer_service_reply.py +0 -0
  299. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_postcompact_recall.py +0 -0
  300. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_process_liveness.py +0 -0
  301. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_protocol_handler.py +0 -0
  302. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_providers_isolation.py +0 -0
  303. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_repl_caller_convention.py +0 -0
  304. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_schedule_command.py +0 -0
  305. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_schedule_create_contract.py +0 -0
  306. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_security.py +0 -0
  307. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_service_app.py +0 -0
  308. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_service_command.py +0 -0
  309. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_service_providers.py +0 -0
  310. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_session_bridge_inject.py +0 -0
  311. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_session_bridge_probe.py +0 -0
  312. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_session_bridge_resolve.py +0 -0
  313. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_session_path_role_gate.py +0 -0
  314. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_smoke_chat.py +0 -0
  315. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_smoke_one_shot.py +0 -0
  316. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_smoke_phase_5_5.py +0 -0
  317. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_spawn_base_hoist.py +0 -0
  318. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_spawn_command.py +0 -0
  319. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_spawn_identity_stamp.py +0 -0
  320. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_spawn_live_pin.py +0 -0
  321. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_spawn_mitosis_lineage.py +0 -0
  322. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_spawn_tmux_session.py +0 -0
  323. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_spawn_windows_relaunch.py +0 -0
  324. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_stall_alert.py +0 -0
  325. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_subprocess_decode_encoding.py +0 -0
  326. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_timeline_command.py +0 -0
  327. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_tmux_wake_submit_verify.py +0 -0
  328. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_token_file_one_parser.py +0 -0
  329. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_token_precedence_explicit_first.py +0 -0
  330. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_unified_token_resolver.py +0 -0
  331. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_version_consistency.py +0 -0
  332. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_vibe_membrane.py +0 -0
  333. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_watchdog_dm_wake.py +0 -0
  334. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_watchdog_dm_wake_cooldown.py +0 -0
  335. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_watchdog_dm_wake_wiring.py +0 -0
  336. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_watchdog_emit_health.py +0 -0
  337. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_watchdog_install_flags.py +0 -0
  338. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_watchdog_model_rung.py +0 -0
  339. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_watchdog_stale_peers.py +0 -0
  340. {swarph_cli-0.48.2 → swarph_cli-0.49.0}/tests/test_windows_waker_installer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swarph-cli
3
- Version: 0.48.2
3
+ Version: 0.49.0
4
4
  Summary: The `swarph` binary — a multi-LLM CLI and mesh-gateway client + bundled server (`swarph gateway`): one-shot prompts across providers, interactive `chat`, multi-provider `spawn` (claude/codex/antigravity via a ProviderMembrane with subprocess billing-scrub), `mesh` send/inbox/register, `brain-ask` over the swarph-brain (gbrain) semantic memory, git-backed `assisted_memory`, per-invocation `gh` identity routing, a silent DM-wake hook, session `import`, and a stranded-session `watchdog`.
5
5
  Author: Pierre Samson, Claude Opus
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "swarph-cli"
7
- version = "0.48.2"
7
+ version = "0.49.0"
8
8
  description = "The `swarph` binary — a multi-LLM CLI and mesh-gateway client + bundled server (`swarph gateway`): one-shot prompts across providers, interactive `chat`, multi-provider `spawn` (claude/codex/antigravity via a ProviderMembrane with subprocess billing-scrub), `mesh` send/inbox/register, `brain-ask` over the swarph-brain (gbrain) semantic memory, git-backed `assisted_memory`, per-invocation `gh` identity routing, a silent DM-wake hook, session `import`, and a stranded-session `watchdog`."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -16,6 +16,6 @@ The architecture splits CLI from substrate so:
16
16
 
17
17
  from __future__ import annotations
18
18
 
19
- __version__ = "0.48.2"
19
+ __version__ = "0.49.0"
20
20
 
21
21
  __all__ = ["__version__"]
@@ -10,7 +10,7 @@ verb so any cell can search the swarm's shared memory the same way. Two modes:
10
10
  Stdlib-only. Config from the environment, mirroring ``swarph mesh``'s token model:
11
11
 
12
12
  GBRAIN_MCP_URL gbrain MCP endpoint; falls back to SWARPH_BRAIN_MCP, else
13
- / SWARPH_BRAIN_MCP http://100.107.222.72:8792/mcp (tailnet IP — gbrain
13
+ / SWARPH_BRAIN_MCP http://<gbrain-host>:8792/mcp (no default — gbrain
14
14
  binds no loopback; measured 2026-08-23, card #548)
15
15
  GBRAIN_TOKEN read token; falls back to SWARPH_BRAIN_TOKEN, then to the
16
16
  / SWARPH_BRAIN_TOKEN mesh per-peer token (~/.config/swarph/<self>.peer_token).
@@ -35,23 +35,34 @@ from swarph_cli import tokens
35
35
  from pathlib import Path
36
36
  from typing import Optional
37
37
 
38
- # TAILNET IP, NOT loopback (card #548): measured 2026-08-23, gbrain binds
39
- # 100.107.222.72:8792 ONLY — 127.0.0.1:8792 refuses on the gateway box itself.
40
- # A loopback default is deaf everywhere, including on the box running gbrain.
41
- _DEFAULT_GBRAIN = "http://100.107.222.72:8792/mcp"
38
+ # NO MODULE-LEVEL ENDPOINT CONSTANT, ON PURPOSE. gbrain binds its tailnet IP
39
+ # only (127.0.0.1:8792 refuses even on gbrain's own box), so SWARPH_BRAIN_MCP must
40
+ # be set explicitly and no host ships as a default (#548 -> #578).
41
+ # `_DEFAULT_GBRAIN = os.environ.get("SWARPH_BRAIN_MCP")` used to live here and was
42
+ # read at IMPORT time, which froze the developer's shell into the module:
43
+ # MEASURED in seat-A review of PR #318 — with SWARPH_BRAIN_MCP exported,
44
+ # test_resolve_endpoint_refuses_rather_than_guessing_a_host FAILED, because the
45
+ # test can delenv the two call-time operands but not a value already captured.
46
+ # The env is read at CALL time in _resolve_endpoint below, and nowhere else.
42
47
  _DEFAULT_TOPK = 6
43
48
 
44
49
 
45
- def _resolve_endpoint() -> str:
46
- """Endpoint precedence: GBRAIN_MCP_URL > SWARPH_BRAIN_MCP > tailnet default.
50
+ def _resolve_endpoint(explicit: str | None = None) -> str:
51
+ """Endpoint precedence: GBRAIN_MCP_URL > SWARPH_BRAIN_MCP. No host default (#578).
47
52
 
48
53
  The SWARPH_BRAIN_MCP fallback keeps the verb config-compatible with the
49
54
  standalone ``swarph-brain-ask`` script (which reads SWARPH_BRAIN_*), so one
50
55
  env config works with both.
51
56
  """
52
- return (os.environ.get("GBRAIN_MCP_URL")
53
- or os.environ.get("SWARPH_BRAIN_MCP")
54
- or _DEFAULT_GBRAIN)
57
+ from swarph_cli.gateway_default import require_gateway
58
+
59
+ return require_gateway(
60
+ explicit
61
+ or os.environ.get("GBRAIN_MCP_URL")
62
+ or os.environ.get("SWARPH_BRAIN_MCP"),
63
+ env="SWARPH_BRAIN_MCP",
64
+ what="gbrain MCP",
65
+ )
55
66
 
56
67
 
57
68
  def _build_query_request(question: str, limit: int = _DEFAULT_TOPK) -> dict:
@@ -237,10 +248,15 @@ def run_brain_ask(argv: list) -> int:
237
248
  help="top-k chunks to retrieve (default 6)")
238
249
  parser.add_argument("--no-synth", action="store_true",
239
250
  help="retrieval only — print raw chunks, skip prose synthesis")
240
- parser.add_argument("--gateway", default=_resolve_endpoint(),
251
+ parser.add_argument("--gateway", default=None,
241
252
  help="gbrain MCP endpoint (env: GBRAIN_MCP_URL or SWARPH_BRAIN_MCP)")
242
253
  parser.add_argument("--token-file", default=None, help="explicit read-token file")
243
254
  args = parser.parse_args(argv)
255
+ # #578/#318-review: resolve the endpoint HERE, not as an argparse default.
256
+ # Evaluating it at parser-BUILD time made `--help` die and made the
257
+ # `--gateway` the error message tells you to pass unreachable, because the
258
+ # parser never got as far as parsing it. Same pattern as ratify:173.
259
+ args.gateway = _resolve_endpoint(args.gateway)
244
260
  question = " ".join(args.question)
245
261
 
246
262
  gw = os.environ.get("SWARPH_BRAIN_GATEWAY")
@@ -290,14 +290,20 @@ def unit_is_relevant(name: str, text: str) -> bool:
290
290
  # cost of the circularity break, stated here rather than discovered.
291
291
 
292
292
  # TAILNET IP, NOT localhost and NOT a MagicDNS name (commander, 2026-08-21).
293
- # The mesh-gateway binds HOST=100.107.222.72 ONLY; localhost has never been bound.
293
+ # The mesh-gateway binds a tailnet IP ONLY; localhost has never been bound.
294
294
  # MESH_GATEWAY_URL overrides — the escape hatch for anyone outside this mesh.
295
- _RESOLVER_DEFAULT_GATEWAY = os.environ.get("MESH_GATEWAY_URL", "http://100.107.222.72:8788")
295
+ # Inlined, NOT imported from swarph_cli.gateway_default: this module is
296
+ # deliberately stdlib-only so it can run as a bare file in isolated mode
297
+ # (test_module_imports_are_stdlib_only). Same policy as #578, no import.
298
+ # NO import-time capture of os.environ here either: `_RESOLVER_DEFAULT_GATEWAY =
299
+ # os.environ.get("MESH_GATEWAY_URL")` used to back this resolver, which made the
300
+ # answer depend on the shell that IMPORTED the module rather than the env passed
301
+ # in. The env is the ARGUMENT — see #578's "model the environment as data".
296
302
 
297
303
 
298
304
  def resolver_gateway(env: dict) -> str:
299
- """What the CLI would use: $MESH_GATEWAY_URL, else the localhost default."""
300
- return (env.get("MESH_GATEWAY_URL") or "").strip() or _RESOLVER_DEFAULT_GATEWAY
305
+ """What the CLI would use: $MESH_GATEWAY_URL, else "" — no host ships (#578)."""
306
+ return (env.get("MESH_GATEWAY_URL") or "").strip()
301
307
 
302
308
 
303
309
  def resolver_token(self_name: str, env: dict, home: Path) -> tuple[Optional[str], str]:
@@ -245,8 +245,8 @@ def _resolve_gateway(arg) -> str:
245
245
 
246
246
  SWARPH_CODEGRAPH_GATEWAY <unset>
247
247
  SWARPH_GATEWAY <unset>
248
- SWARPH_BRAIN_GATEWAY http://100.107.222.72:8788 <- brain-NAMED, MESH value
249
- MESH_GATEWAY_URL http://100.107.222.72:8788 <- correct, and NOT consulted
248
+ SWARPH_BRAIN_GATEWAY http://<host>:8788 <- brain-NAMED, MESH value
249
+ MESH_GATEWAY_URL http://<host>:8788 <- correct, and NOT consulted
250
250
 
251
251
  The relay worked ONLY because a brain-named variable was mis-set to the mesh
252
252
  gateway's address. Every link that resolved was unset or wrong-by-name, and the
@@ -36,8 +36,8 @@ import urllib.error
36
36
  import urllib.request
37
37
  from pathlib import Path
38
38
  from typing import Optional
39
+ from swarph_cli.gateway_default import env_gateway
39
40
 
40
- DEFAULT_GATEWAY = os.environ.get("MESH_GATEWAY_URL", "http://100.107.222.72:8788")
41
41
  TIMEOUT_S = 6
42
42
  MAX_ROWS = 6
43
43
 
@@ -131,6 +131,14 @@ def query_gateway(term: str, gateway: str, token: str, limit: int = MAX_ROWS) ->
131
131
  >>> AN UNREACHABLE OR REFUSING BACKEND IS AN ERROR, NOT AN EMPTY RESULT. <<<
132
132
  Collapsing them is the defect this hook exists to avoid teaching.
133
133
  """
134
+ # #578: with no baked-in host, `gateway` can be "". Request() rejects a
135
+ # relative URL at CONSTRUCTION (below, outside the try), so an unconfigured
136
+ # backend would raise ValueError and take the whole turn down — against this
137
+ # entry point's "ALWAYS exits 0, must never fail a turn" contract.
138
+ # UNCONFIGURED is the same class as UNREACHABLE: an error, not empty.
139
+ if not (gateway or "").strip():
140
+ return {"error": "MESH_GATEWAY_URL is not set and swarph ships no default "
141
+ "gateway host (#578) — the graph was never asked"}
134
142
  req = urllib.request.Request(
135
143
  f"{gateway.rstrip('/')}/codegraph",
136
144
  data=json.dumps({"query": term, "limit": limit}).encode(),
@@ -228,7 +236,7 @@ def run_codegraph_hook(argv: Optional[list] = None) -> int:
228
236
  """PostToolUse(Bash) entry point. ALWAYS exits 0 — it must never fail a turn."""
229
237
  argv = list(argv or [])
230
238
  self_name = os.environ.get("SWARPH_SELF", "").strip()
231
- gateway = DEFAULT_GATEWAY
239
+ gateway = env_gateway()
232
240
  for i, a in enumerate(argv):
233
241
  if a == "--as" and i + 1 < len(argv):
234
242
  self_name = argv[i + 1]
@@ -52,6 +52,7 @@ from typing import Optional
52
52
  from swarph_cli import session_bridge, stall_alert
53
53
  from swarph_cli.console_safe import print_safe
54
54
  from swarph_cli.delivery_queue import DeliveryQueue
55
+ from swarph_cli.gateway_default import env_gateway, require_gateway
55
56
 
56
57
 
57
58
  _DEFAULT_POLL_S = 30
@@ -85,7 +86,7 @@ def _build_parser() -> argparse.ArgumentParser:
85
86
  )
86
87
  p.add_argument(
87
88
  "--gateway",
88
- default=os.environ.get("MESH_GATEWAY_URL", "http://100.107.222.72:8788"),
89
+ default=env_gateway(),
89
90
  help="mesh-gateway base URL.",
90
91
  )
91
92
  p.add_argument(
@@ -564,6 +565,10 @@ def run_daemon(argv: list[str]) -> int:
564
565
  state_dir.mkdir(parents=True, exist_ok=True)
565
566
  # Announce ownership of this state dir BEFORE any cursor work, so a
566
567
  # concurrently-starting `swarph monitor` sees it and refuses (PR #139).
568
+ # #578: identity is resolved above first, so a missing --self reports THAT,
569
+ # not a missing env var. The gateway is only needed for the requests below.
570
+ args.gateway = require_gateway(getattr(args, "gateway", ""))
571
+
567
572
  _write_daemon_pidfile(state_dir)
568
573
  token = _resolve_token(args.token_file)
569
574
  if not token:
@@ -67,9 +67,12 @@ def _post_channel(
67
67
  """
68
68
  name = resolve_self_name(self_name)
69
69
  token = resolve_token(name, token_file)
70
- from .mesh import _DEFAULT_GATEWAY
70
+ from swarph_cli.gateway_default import env_gateway
71
71
 
72
- base = (gateway or _DEFAULT_GATEWAY).rstrip("/")
72
+ # "" when unconfigured — post_json IS mesh._post_json, whose
73
+ # _require_absolute_gateway_url then refuses by name rather than raising
74
+ # `ValueError: unknown url type` from Request(). Checked in review of #318.
75
+ base = (gateway or env_gateway()).rstrip("/")
73
76
  ev: dict = {"event": event, "payload": payload}
74
77
  if chain_token is not None: # carry an explicit "" through; only omit when unset
75
78
  ev["chain_token"] = chain_token
@@ -22,8 +22,8 @@ import yaml
22
22
 
23
23
  from swarph_shared.cell import PEER_NAME_RE, parse_cell_dict
24
24
  from swarph_cli.cell import CLI_ENABLED_PROVIDERS, CellError, cells_dir
25
+ from swarph_cli.gateway_default import env_gateway
25
26
 
26
- _DEFAULT_GATEWAY = os.environ.get("MESH_GATEWAY_URL", "http://100.107.222.72:8788")
27
27
  _CODEX_SANDBOX_DEFAULT = "workspace-write"
28
28
  _CODEX_SANDBOX_VALUES = ("workspace-write", "read-only")
29
29
 
@@ -128,7 +128,7 @@ def _build_parser() -> argparse.ArgumentParser:
128
128
  p.add_argument("--tmux", default=None)
129
129
  p.add_argument("--cursor", default=None)
130
130
  p.add_argument("--sandbox", default=None)
131
- p.add_argument("--gateway", default=_DEFAULT_GATEWAY)
131
+ p.add_argument("--gateway", default=env_gateway())
132
132
  p.add_argument("--assisted-memory", dest="assisted_memory", default=None, metavar="REPO",
133
133
  help="enable git-backed memory at REPO (SSH→HTTPS normalized)")
134
134
  p.add_argument("--starter", default=None)
@@ -197,8 +197,11 @@ def run_init(argv: list[str]) -> int:
197
197
  "cwd": str(cwd),
198
198
  "tmux_session": tmux,
199
199
  "cursor_path": cursor,
200
- "mesh": {"gateway": args.gateway},
201
200
  }
201
+ # Only when one was actually chosen: with the env unset args.gateway is ""
202
+ # (#578 ships no host), and writing that records a decision nobody made.
203
+ if args.gateway:
204
+ cell["mesh"] = {"gateway": args.gateway}
202
205
  if sandbox is not None:
203
206
  cell["sandbox"] = sandbox
204
207
  if args.starter:
@@ -39,6 +39,7 @@ from typing import Optional
39
39
  from .. import tokens
40
40
  from ._content import ContentError, add_content_args, resolve_content
41
41
  from ._display import sanitize_terminal
42
+ from swarph_cli.gateway_default import GatewayNotConfigured, env_gateway
42
43
 
43
44
 
44
45
  def _format_inbox_line(dm: dict) -> str:
@@ -51,17 +52,13 @@ def _format_inbox_line(dm: dict) -> str:
51
52
  return f"id={dm.get('id')} {read} from={who} kind={kind} {content}"
52
53
 
53
54
 
54
- # TAILNET IP, NOT localhost and NOT a MagicDNS name (commander, 2026-08-21).
55
- # The mesh-gateway binds HOST=100.107.222.72 ONLY — localhost has never been bound, so this
56
- # default failed as a bare "Connection refused" with no cause named. It cost 792 silent
57
- # card-export failures over 8 days, and it turned a WORKING hand-started monitor into a
58
- # DEAF SUPERVISED one the moment it was moved to a systemd unit (the unit passes no
59
- # --gateway, so it fell through to this constant).
60
- # An IP over MagicDNS on purpose: a name needs MagicDNS enabled, the right search domain,
61
- # and no local collision; the IP needs only that tailscale is up, which is the real
62
- # precondition anyway. MESH_GATEWAY_URL overrides it — that env var is the escape hatch
63
- # for anyone outside this mesh, and optional inside it.
64
- _DEFAULT_GATEWAY = os.environ.get("MESH_GATEWAY_URL", "http://100.107.222.72:8788")
55
+ # NO MODULE-LEVEL GATEWAY CONSTANT (#578). #546 put the gateway's tailnet IP here,
56
+ # correctly at the time; that box was retired 2026-08-25 and the literal expired.
57
+ # The constant is gone rather than emptied: read at IMPORT time it captured whatever
58
+ # the packaging/importing shell exported, and MEASURED in seat-A review of PR #318 it
59
+ # still returned that value after the environment was cleared — the one channel by
60
+ # which a test could depend on a developer's shell. env_gateway() is called at
61
+ # parser-BUILD time instead (parsers are built per invocation).
65
62
  _DEFAULT_POLL_S = 30
66
63
  _BACKOFF_EMPTY_THRESHOLD = 5
67
64
  _BACKOFF_EMPTY_SECONDS = 60
@@ -170,7 +167,7 @@ def _build_parser() -> argparse.ArgumentParser:
170
167
  )
171
168
  peers.add_argument(
172
169
  "--gateway",
173
- default=os.environ.get("MESH_GATEWAY_URL", _DEFAULT_GATEWAY),
170
+ default=env_gateway(),
174
171
  help="mesh-gateway base URL",
175
172
  )
176
173
  peers.add_argument("--token-file", default=None, help="explicit bearer token file")
@@ -200,7 +197,7 @@ def _add_common(p: argparse.ArgumentParser) -> None:
200
197
  )
201
198
  p.add_argument(
202
199
  "--gateway",
203
- default=os.environ.get("MESH_GATEWAY_URL", _DEFAULT_GATEWAY),
200
+ default=env_gateway(),
204
201
  help="mesh-gateway base URL",
205
202
  )
206
203
  p.add_argument("--token-file", default=None, help="explicit bearer token file")
@@ -313,12 +310,69 @@ def _resolve_token(
313
310
  )
314
311
  if res is not None:
315
312
  return res.token
313
+ # #579: the old message named the very file `register` exists to MINT, which
314
+ # reads as a chicken-and-egg to anyone bootstrapping a NEW peer (droplet hit
315
+ # exactly this onboarding friendly-coder, 2026-08-25). `swarph onboard`
316
+ # already explains the operator path; this now says the same thing.
317
+ #
318
+ # >>> THE BOOTSTRAP PARAGRAPH BELONGS TO `register` ALONE. <<< It first shipped
319
+ # unconditionally, so `mesh send`/`reply`/`inbox`/`sidecar` — which all reach here
320
+ # with the default allow_peer_token=True — told a user with a missing token that
321
+ # "minting it is what this command does", which is FALSE for send. Measured in
322
+ # seat-A review of PR #318. allow_peer_token=False is exactly the register path
323
+ # (:848 is its only caller), so it is the condition, not a new flag.
324
+ bootstrap = (
325
+ f"\n"
326
+ f" If you are BOOTSTRAPPING {self_name!r}, that token does not exist yet:\n"
327
+ f" minting it is what this command does, so it cannot also be the way in.\n"
328
+ f" Registering a peer that is not yourself is an OPERATOR action. Where the\n"
329
+ f" gateway enforces caller binding (MESH_CALLER_BINDING_ENFORCE=1, as this\n"
330
+ f" mesh's does) another cell's per-peer token 403s; where it does not, it is\n"
331
+ f" simply the wrong credential. `swarph gateway serve` does NOT enforce it by\n"
332
+ f" default — do not read the 403 as a guarantee.\n"
333
+ f" swarph mesh register --as {self_name} --token-file <operator-token>\n"
334
+ f" MESH_GATEWAY_TOKEN=<operator-token> swarph mesh register --as {self_name}"
335
+ ) if not allow_peer_token else (
336
+ f"\n"
337
+ f" If it does not exist yet, {self_name!r} has no credential on this mesh —\n"
338
+ f" `swarph mesh register` (an OPERATOR action) is what mints one."
339
+ )
316
340
  raise RuntimeError(
317
- "cannot resolve mesh token; set MESH_GATEWAY_TOKEN or create "
318
- f"{_peer_token_path(self_name)}"
341
+ f"cannot resolve a mesh credential for {self_name!r}.\n"
342
+ f"\n"
343
+ f" looked for: ${{MESH_GATEWAY_TOKEN}}\n"
344
+ f" {_peer_token_path(self_name)}\n"
345
+ f"\n"
346
+ f" If {self_name!r} ALREADY EXISTS, its per-peer token is the right\n"
347
+ f" credential — place it at the path above (mode 600) and re-run.\n"
348
+ + bootstrap
319
349
  )
320
350
 
321
351
 
352
+
353
+ def _require_absolute_gateway_url(url: str) -> None:
354
+ """#578: no gateway host ships as a default.
355
+
356
+ Guarding here rather than at the dispatcher is deliberate: the gateway is
357
+ needed only when a REQUEST is made. A dispatcher-level check also fired for
358
+ paths that never touch the network (sidecar target validation, --json shape
359
+ tests) and turned 10 failures into 26. The observable that means
360
+ "unconfigured" is a url that is not absolute — `/peers` instead of
361
+ `http://host:8788/peers` — so key on that.
362
+ """
363
+ if not str(url).startswith(("http://", "https://")):
364
+ # GatewayNotConfigured (a RuntimeError) rather than a bare RuntimeError:
365
+ # cli used to carry TWO refusal shapes for one variable and one remedy —
366
+ # require_gateway at dispatch and this one at request time — so the package
367
+ # disagreed with itself about what "no gateway" raises. One family now.
368
+ raise GatewayNotConfigured(
369
+ "MESH_GATEWAY_URL is not set, and swarph ships no default gateway host.\n"
370
+ " A baked-in address expires the day that box is retired "
371
+ "(card #578; it happened on 2026-08-25).\n"
372
+ " Set MESH_GATEWAY_URL=http://<host>:<port>, or pass --gateway.\n"
373
+ f" (tried to request {url!r})"
374
+ )
375
+
322
376
  def _post_json(
323
377
  url: str,
324
378
  body: dict,
@@ -326,6 +380,7 @@ def _post_json(
326
380
  *,
327
381
  timeout: float = 10.0,
328
382
  ) -> tuple[int, dict]:
383
+ _require_absolute_gateway_url(url)
329
384
  data = json.dumps(body).encode("utf-8")
330
385
  req = urllib.request.Request(
331
386
  url,
@@ -355,6 +410,7 @@ def _http_get_json(
355
410
  *,
356
411
  timeout: float = 10.0,
357
412
  ) -> tuple[int, dict]:
413
+ _require_absolute_gateway_url(url)
358
414
  req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"})
359
415
  try:
360
416
  with urllib.request.urlopen(req, timeout=timeout) as resp:
@@ -43,6 +43,8 @@ import sys
43
43
  import time
44
44
  from pathlib import Path
45
45
 
46
+ from swarph_cli.gateway_default import env_gateway
47
+
46
48
  from . import mesh
47
49
 
48
50
 
@@ -122,7 +124,7 @@ def _add_common(p: argparse.ArgumentParser) -> None:
122
124
  p.add_argument("--state-dir", default=None, help="state directory")
123
125
  p.add_argument(
124
126
  "--gateway",
125
- default=os.environ.get("MESH_GATEWAY_URL", mesh._DEFAULT_GATEWAY),
127
+ default=env_gateway(),
126
128
  help="mesh-gateway base URL",
127
129
  )
128
130
  p.add_argument("--token-file", default=None, help="explicit bearer token file")
@@ -35,6 +35,7 @@ from pathlib import Path
35
35
  from typing import Optional
36
36
  from swarph_cli import tokens
37
37
  from swarph_cli.console_safe import print_safe
38
+ from swarph_cli.gateway_default import env_gateway, require_gateway
38
39
 
39
40
 
40
41
  _HANDSHAKE_TEMPLATE = """\
@@ -111,8 +112,8 @@ def _build_parser() -> argparse.ArgumentParser:
111
112
  )
112
113
  p.add_argument(
113
114
  "--gateway",
114
- default=os.environ.get("MESH_GATEWAY_URL", "http://100.107.222.72:8788"),
115
- help="mesh-gateway base URL (default: $MESH_GATEWAY_URL or http://100.107.222.72:8788)",
115
+ default=env_gateway(),
116
+ help="mesh-gateway base URL (default: $MESH_GATEWAY_URL; no host is shipped)",
116
117
  )
117
118
  p.add_argument(
118
119
  "--token-file",
@@ -824,6 +825,9 @@ def run_onboard(argv: list[str]) -> int:
824
825
 
825
826
  # ── Step 4: POST /peers/register ─────────────────────────────────
826
827
  peer_url = args.url or f"http://{canonical}:8787"
828
+ # #578: the gateway is needed only for the REQUEST. Validate args first so a
829
+ # typo'd name reports the typo, not a missing env var.
830
+ args.gateway = require_gateway(getattr(args, "gateway", ""))
827
831
  print_safe(f"[4/6] POST {args.gateway}/peers/register")
828
832
  # >>> #124's GUARD, MET CLIENT-SIDE — #294's PATTERN, THE MISSING HALF. <<<
829
833
  # The gateway 409s a re-register whose payload would DROP stored
@@ -26,6 +26,7 @@ import urllib.error
26
26
  import urllib.request
27
27
  from pathlib import Path
28
28
  from typing import Optional
29
+ from swarph_cli.gateway_default import env_gateway, require_gateway
29
30
 
30
31
 
31
32
  def _build_parser() -> argparse.ArgumentParser:
@@ -57,7 +58,7 @@ def _build_parser() -> argparse.ArgumentParser:
57
58
  )
58
59
  p.add_argument(
59
60
  "--gateway",
60
- default=os.environ.get("MESH_GATEWAY_URL", "http://100.107.222.72:8788"),
61
+ default=env_gateway(),
61
62
  help="mesh-gateway base URL.",
62
63
  )
63
64
  p.add_argument(
@@ -167,6 +168,9 @@ def run_ratify(argv: list[str]) -> int:
167
168
  return 1
168
169
 
169
170
  # ── Step 2: caller-side gate (witness must itself be ratified) ───
171
+ # #578: the gateway is needed only for the REQUEST. Arg validation above runs
172
+ # first, so a bad name reports the bad name, not a missing env var.
173
+ args.gateway = require_gateway(getattr(args, "gateway", ""))
170
174
  print(f"[2/6] verify witness {witness!r} is ratified")
171
175
  status, witness_body = _http_json(
172
176
  f"{args.gateway}/peers/{witness}", token=token
@@ -77,6 +77,7 @@ import urllib.request
77
77
  from datetime import datetime, timedelta, timezone
78
78
  from pathlib import Path
79
79
  from typing import Optional
80
+ from swarph_cli.gateway_default import env_gateway
80
81
 
81
82
  from swarph_cli.commands.mesh import _post_json
82
83
 
@@ -84,17 +85,13 @@ from swarph_cli.commands.mesh import _post_json
84
85
  _DEFAULT_THRESHOLD_SEC = 1800 # 30 minutes
85
86
  _DEFAULT_A1_RETRIES = 3
86
87
  _DEFAULT_A1_BACKOFF_SEC = 60
87
- # TAILNET IP, NOT localhost and NOT a MagicDNS name (commander, 2026-08-21).
88
- # The mesh-gateway binds HOST=100.107.222.72 ONLY — localhost has never been bound, so this
89
- # default failed as a bare "Connection refused" with no cause named. It cost 792 silent
90
- # card-export failures over 8 days, and it turned a WORKING hand-started monitor into a
91
- # DEAF SUPERVISED one the moment it was moved to a systemd unit (the unit passes no
92
- # --gateway, so it fell through to this constant).
93
- # An IP over MagicDNS on purpose: a name needs MagicDNS enabled, the right search domain,
94
- # and no local collision; the IP needs only that tailscale is up, which is the real
95
- # precondition anyway. MESH_GATEWAY_URL overrides it — that env var is the escape hatch
96
- # for anyone outside this mesh, and optional inside it.
97
- _DEFAULT_GATEWAY_URL = os.environ.get("MESH_GATEWAY_URL", "http://100.107.222.72:8788")
88
+ # NO MODULE-LEVEL GATEWAY CONSTANT (#578). #546 put the gateway's tailnet IP here,
89
+ # correctly at the time; that box was retired 2026-08-25 and the literal expired.
90
+ # The constant is gone rather than emptied: read at IMPORT time it captured whatever
91
+ # the packaging/importing shell exported, and MEASURED in seat-A review of PR #318 it
92
+ # still returned that value after the environment was cleared — the one channel by
93
+ # which a test could depend on a developer's shell. env_gateway() is called at
94
+ # parser-BUILD time instead (parsers are built per invocation).
98
95
  # F3 — tmux pane_activity gate threshold. If pane has activity within this
99
96
  # many seconds, suppress A1 (session is working, not stalled). 600s (10min)
100
97
  # is comfortably above legitimate-pause noise + comfortably below the
@@ -197,7 +194,7 @@ Flags:
197
194
  --cursor PATH cursor JSON path; default $TMPDIR/<role>-cursor.json
198
195
  fallback /tmp/lab-claude-cursor.json
199
196
  --threshold SEC darkness threshold; default 1800 (30 min)
200
- --gateway URL mesh-gateway URL for unread-DM check; default 100.107.222.72:8788
197
+ --gateway URL mesh-gateway URL for unread-DM check; default $MESH_GATEWAY_URL
201
198
  --tmux-session NAME tmux session name; default = cell role
202
199
  --peer NAME mesh peer name for unread-DM query; default = cell name
203
200
  --no-respawn A1 only; don't escalate to A2 (dry-run mode)
@@ -437,6 +434,28 @@ def _save_dm_wake_state(path: Path, state: dict) -> None:
437
434
  pass
438
435
 
439
436
 
437
+
438
+ def _gateway_configured(gateway: str) -> bool:
439
+ """False when no gateway is configured — degrade, do not crash.
440
+
441
+ #578 removed the baked-in host, so `gateway` can now be "". Every caller here
442
+ builds `urllib.request.Request(f"{gateway}/...")` OUTSIDE its try, and
443
+ Request() rejects a relative URL at CONSTRUCTION with ValueError, before any
444
+ I/O — so nothing catches it.
445
+
446
+ That matters most where it is least visible: `_run_local_check` asks for the
447
+ unread count BEFORE the A2 "process dead -> respawn regardless of unread"
448
+ decision, so an uncaught raise kills the tick before it can rescue anything.
449
+ On sys3 today both `swarph-watchdog.timer` and `swarph-watchdog-science.timer`
450
+ run every 5 minutes with `EnvironmentFile=-/etc/default/swarph-watchdog`, and
451
+ THAT FILE DOES NOT EXIST — no MESH_GATEWAY_URL in the unit env at all.
452
+
453
+ An unreachable gateway has always degraded to None here. An UNCONFIGURED one
454
+ now does the same, rather than becoming a new crash class.
455
+ Found by drop-on-meta-edge, seat-A review of PR #318.
456
+ """
457
+ return bool((gateway or "").strip())
458
+
440
459
  def _gateway_unread_count(gateway: str, peer: str, token: Optional[str]) -> Optional[int]:
441
460
  """Query gateway for unread DM count addressed to peer.
442
461
 
@@ -453,6 +472,8 @@ def _gateway_unread_count(gateway: str, peer: str, token: Optional[str]) -> Opti
453
472
  caused a real reader to conclude a gateway/token outage makes A1 MORE
454
473
  eager, when it makes A1 permanently inert for that cell instead.
455
474
  """
475
+ if not _gateway_configured(gateway):
476
+ return None
456
477
  query = urllib.parse.urlencode({"to_node": peer, "unread_only": "true", "limit": 1})
457
478
  url = f"{gateway.rstrip('/')}/messages?{query}"
458
479
  req = urllib.request.Request(url)
@@ -510,6 +531,8 @@ def _gateway_recent_recovery_event(
510
531
  What was missing was the wake-up mechanism — this function plus the
511
532
  fall-through in run_check is the watchdog half of the loop.
512
533
  """
534
+ if not _gateway_configured(gateway):
535
+ return None
513
536
  since_dt = datetime.now(timezone.utc) - timedelta(seconds=window_sec)
514
537
  since_iso = since_dt.isoformat()
515
538
  query = urllib.parse.urlencode(
@@ -589,6 +612,8 @@ def _stale_peers(
589
612
  def _fetch_peers(gateway: str, token: str) -> list[dict]:
590
613
  """GET {gateway}/peers (Bearer token). Returns the peer list, or [] on any error
591
614
  (never raises). The response may be a bare list OR {"peers": [...]} — handle both."""
615
+ if not _gateway_configured(gateway):
616
+ return None
592
617
  url = f"{gateway.rstrip('/')}/peers"
593
618
  req = urllib.request.Request(url)
594
619
  if token:
@@ -1710,7 +1735,7 @@ def _execstart_flags(args: argparse.Namespace) -> str:
1710
1735
  ("--peer", "peer", None),
1711
1736
  ("--liveness-cmd", "liveness_cmd", None),
1712
1737
  ("--notify-peer", "notify_peer", None),
1713
- ("--gateway", "gateway", _DEFAULT_GATEWAY_URL),
1738
+ ("--gateway", "gateway", env_gateway()),
1714
1739
  ("--threshold", "threshold", _DEFAULT_THRESHOLD_SEC),
1715
1740
  ("--pane-activity-threshold", "pane_activity_threshold",
1716
1741
  _DEFAULT_PANE_ACTIVITY_THRESHOLD_SEC),
@@ -1914,7 +1939,7 @@ def _build_parser() -> argparse.ArgumentParser:
1914
1939
  )
1915
1940
  p.add_argument(
1916
1941
  "--gateway",
1917
- default=os.environ.get("MESH_GATEWAY_URL", _DEFAULT_GATEWAY_URL),
1942
+ default=env_gateway(),
1918
1943
  )
1919
1944
  p.add_argument("--tmux-session", default=None)
1920
1945
  p.add_argument("--peer", default=None)