swarph-cli 0.25.0__tar.gz → 0.26.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 (156) hide show
  1. {swarph_cli-0.25.0/src/swarph_cli.egg-info → swarph_cli-0.26.0}/PKG-INFO +1 -1
  2. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/pyproject.toml +1 -1
  3. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/__init__.py +1 -1
  4. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/brain_ask.py +42 -13
  5. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/gateway/server.py +61 -0
  6. {swarph_cli-0.25.0 → swarph_cli-0.26.0/src/swarph_cli.egg-info}/PKG-INFO +1 -1
  7. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli.egg-info/SOURCES.txt +1 -0
  8. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_brain_ask_command.py +59 -0
  9. swarph_cli-0.26.0/tests/test_gateway_brain.py +104 -0
  10. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_watchdog.py +2 -2
  11. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/LICENSE +0 -0
  12. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/README.md +0 -0
  13. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/setup.cfg +0 -0
  14. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/caller.py +0 -0
  15. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/capture/__init__.py +0 -0
  16. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/capture/harden.py +0 -0
  17. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/capture/lineage.py +0 -0
  18. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/capture/liveness.py +0 -0
  19. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/capture/manifest.py +0 -0
  20. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/capture/paths.py +0 -0
  21. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/capture/verify.py +0 -0
  22. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/cell.py +0 -0
  23. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/chain_token.py +0 -0
  24. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/__init__.py +0 -0
  25. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/_gateway_client.py +0 -0
  26. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/add.py +0 -0
  27. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/brain.py +0 -0
  28. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/cell.py +0 -0
  29. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/channel.py +0 -0
  30. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/chat.py +0 -0
  31. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/codegraph.py +0 -0
  32. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/compress.py +0 -0
  33. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/daemon.py +0 -0
  34. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/event.py +0 -0
  35. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/gateway.py +0 -0
  36. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/highlight.py +0 -0
  37. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/hook_output.py +0 -0
  38. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/hooks.py +0 -0
  39. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/import_session.py +0 -0
  40. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/init.py +0 -0
  41. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/install_hook.py +0 -0
  42. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/install_multiplexer.py +0 -0
  43. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/lane.py +0 -0
  44. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/mcp_server.py +0 -0
  45. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/memory_sync.py +0 -0
  46. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/mesh.py +0 -0
  47. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/onboard.py +0 -0
  48. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/protocol_handler.py +0 -0
  49. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/ratify.py +0 -0
  50. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/schedule.py +0 -0
  51. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/security.py +0 -0
  52. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/service.py +0 -0
  53. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/spawn.py +0 -0
  54. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/watchdog.py +0 -0
  55. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/compress/__init__.py +0 -0
  56. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/compress/levers.py +0 -0
  57. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/compress/marker.py +0 -0
  58. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/compress/verify.py +0 -0
  59. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/gateway/__init__.py +0 -0
  60. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/gateway/feature_registry.py +0 -0
  61. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/gateway/lanes_control.py +0 -0
  62. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/gateway/schema.sql +0 -0
  63. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/gateway/services_control.py +0 -0
  64. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/main.py +0 -0
  65. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/multiplexer.py +0 -0
  66. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/parsers/__init__.py +0 -0
  67. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/parsers/claude.py +0 -0
  68. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/service/__init__.py +0 -0
  69. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/service/app.py +0 -0
  70. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/service/providers.py +0 -0
  71. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
  72. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
  73. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
  74. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
  75. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli.egg-info/entry_points.txt +0 -0
  76. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli.egg-info/requires.txt +0 -0
  77. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli.egg-info/top_level.txt +0 -0
  78. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_add_security_gate.py +0 -0
  79. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_artifact_add.py +0 -0
  80. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_artifact_hash.py +0 -0
  81. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_artifact_lib.py +0 -0
  82. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_artifact_mcp.py +0 -0
  83. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_artifact_skill.py +0 -0
  84. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_artifact_tool.py +0 -0
  85. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_artifact_uri.py +0 -0
  86. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_brain_command.py +0 -0
  87. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_caller_meta_guard.py +0 -0
  88. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_capture_lineage.py +0 -0
  89. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_capture_liveness.py +0 -0
  90. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_capture_manifest.py +0 -0
  91. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_capture_paths.py +0 -0
  92. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_capture_security.py +0 -0
  93. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_cell_command_dispatch.py +0 -0
  94. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_cell_harden.py +0 -0
  95. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_cell_loader.py +0 -0
  96. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_cell_verify.py +0 -0
  97. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_chain_token.py +0 -0
  98. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_channel_command.py +0 -0
  99. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_chat_command.py +0 -0
  100. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_claude_parser.py +0 -0
  101. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_claude_tmux_template.py +0 -0
  102. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_codegraph_command.py +0 -0
  103. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_codegraph_mcp.py +0 -0
  104. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_compress_caller_convention.py +0 -0
  105. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_compress_command.py +0 -0
  106. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_compress_levers.py +0 -0
  107. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_compress_marker.py +0 -0
  108. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_compress_verify.py +0 -0
  109. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_daemon_command.py +0 -0
  110. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_event_emit.py +0 -0
  111. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_feature_to_uri.py +0 -0
  112. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_gateway_client.py +0 -0
  113. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_gateway_command.py +0 -0
  114. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_highlight_command.py +0 -0
  115. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_hook_output.py +0 -0
  116. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_hooks_add.py +0 -0
  117. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_hooks_bundle.py +0 -0
  118. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_hooks_init.py +0 -0
  119. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_hooks_lifecycle.py +0 -0
  120. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_hooks_merge.py +0 -0
  121. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_import_command.py +0 -0
  122. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_init_command.py +0 -0
  123. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_install_hook.py +0 -0
  124. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_install_multiplexer_command.py +0 -0
  125. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_lane_command.py +0 -0
  126. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_main.py +0 -0
  127. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_mcp_server.py +0 -0
  128. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_memory_sync.py +0 -0
  129. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_mesh_command.py +0 -0
  130. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_mesh_sidecar.py +0 -0
  131. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_meta_edge_identity.py +0 -0
  132. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_multiplexer.py +0 -0
  133. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_onboard_command.py +0 -0
  134. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_protocol_handler.py +0 -0
  135. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_ratify_command.py +0 -0
  136. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_repl_caller_convention.py +0 -0
  137. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_schedule_command.py +0 -0
  138. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_security.py +0 -0
  139. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_service_app.py +0 -0
  140. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_service_command.py +0 -0
  141. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_service_providers.py +0 -0
  142. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_session_path_role_gate.py +0 -0
  143. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_smoke_chat.py +0 -0
  144. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_smoke_one_shot.py +0 -0
  145. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_smoke_phase_5_5.py +0 -0
  146. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_spawn_command.py +0 -0
  147. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_spawn_live_pin.py +0 -0
  148. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_spawn_mitosis_lineage.py +0 -0
  149. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_spawn_tmux_session.py +0 -0
  150. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_spawn_windows_relaunch.py +0 -0
  151. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_version_consistency.py +0 -0
  152. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_watchdog_dm_wake.py +0 -0
  153. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_watchdog_dm_wake_cooldown.py +0 -0
  154. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_watchdog_dm_wake_wiring.py +0 -0
  155. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_watchdog_model_rung.py +0 -0
  156. {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_watchdog_stale_peers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swarph-cli
3
- Version: 0.25.0
3
+ Version: 0.26.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`, 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.25.0"
7
+ version = "0.26.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`, 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.25.0"
19
+ __version__ = "0.26.0"
20
20
 
21
21
  __all__ = ["__version__"]
@@ -15,6 +15,9 @@ Stdlib-only. Config from the environment, mirroring ``swarph mesh``'s token mode
15
15
  / SWARPH_BRAIN_TOKEN mesh per-peer token (~/.config/swarph/<self>.peer_token).
16
16
  / peer-token file Once gbrain accepts mesh peer tokens, the peer token IS
17
17
  the read token — no separate secret to provision.
18
+ SWARPH_BRAIN_GATEWAY when set, query the brain via the mesh gateway's
19
+ /brain/query proxy using the cell's mesh peer token
20
+ (no per-cell gbrain_ token). Unset = direct :8792.
18
21
  SWARPH_FACADE optional synthesis endpoint (claude-service chat-completions)
19
22
  SWARPH_FACADE_TOKEN bearer for the facade
20
23
  """
@@ -127,6 +130,15 @@ def _mcp_query(url: str, token: str, question: str, limit: int) -> list:
127
130
  return _parse_query_response(raw)
128
131
 
129
132
 
133
+ def _gateway_query(gw_base: str, peer_token: str, question: str, limit: int) -> list:
134
+ """Query the brain via the mesh gateway's /brain/query proxy, authenticating
135
+ with the cell's MESH peer token. The gateway holds the gbrain token; we never do."""
136
+ url = gw_base.rstrip("/") + "/brain/query"
137
+ raw = _http_post(url, {"query": question, "limit": limit}, peer_token,
138
+ accept="application/json")
139
+ return json.loads(raw).get("chunks", [])
140
+
141
+
130
142
  def _synthesize(facade_url: str, facade_token: str, question: str, chunks: list) -> str:
131
143
  """Ask the $0 facade to answer ONLY from the retrieved chunks, citing slugs."""
132
144
  context = _format_chunks(chunks)
@@ -158,19 +170,36 @@ def run_brain_ask(argv: list) -> int:
158
170
  args = parser.parse_args(argv)
159
171
  question = " ".join(args.question)
160
172
 
161
- token = _resolve_token(args.token_file, _self_name())
162
- if not token:
163
- sys.stderr.write(
164
- "swarph brain-ask: no gbrain read token "
165
- "(set GBRAIN_TOKEN / SWARPH_BRAIN_TOKEN, pass --token-file, or place a "
166
- "mesh peer token at ~/.config/swarph/<self>.peer_token)\n")
167
- return 2
168
-
169
- try:
170
- chunks = _mcp_query(args.gateway, token, question, args.limit)
171
- except Exception as exc: # noqa: BLE001 — surface any transport/parse failure cleanly
172
- sys.stderr.write(f"swarph brain-ask: gbrain query failed: {exc}\n")
173
- return 1
173
+ gw = os.environ.get("SWARPH_BRAIN_GATEWAY")
174
+ if gw:
175
+ self_name = _self_name()
176
+ try:
177
+ peer_token = _peer_token_path(self_name).read_text(encoding="utf-8").strip()
178
+ except OSError:
179
+ peer_token = ""
180
+ if not peer_token:
181
+ sys.stderr.write(
182
+ f"swarph brain-ask: SWARPH_BRAIN_GATEWAY set but no mesh peer token at "
183
+ f"~/.config/swarph/{self_name}.peer_token\n")
184
+ return 2
185
+ try:
186
+ chunks = _gateway_query(gw, peer_token, question, args.limit)
187
+ except Exception as e: # noqa: BLE001 — surface, don't swallow
188
+ sys.stderr.write(f"swarph brain-ask: gateway brain query failed: {e}\n")
189
+ return 1
190
+ else:
191
+ token = _resolve_token(args.token_file, _self_name())
192
+ if not token:
193
+ sys.stderr.write(
194
+ "swarph brain-ask: no gbrain read token "
195
+ "(set GBRAIN_TOKEN / SWARPH_BRAIN_TOKEN, pass --token-file, or place a "
196
+ "mesh peer token at ~/.config/swarph/<self>.peer_token)\n")
197
+ return 2
198
+ try:
199
+ chunks = _mcp_query(args.gateway, token, question, args.limit)
200
+ except Exception as exc: # noqa: BLE001 — surface any transport/parse failure cleanly
201
+ sys.stderr.write(f"swarph brain-ask: gbrain query failed: {exc}\n")
202
+ return 1
174
203
 
175
204
  facade = os.environ.get("SWARPH_FACADE")
176
205
  if args.no_synth or not facade:
@@ -41,6 +41,7 @@ import sqlite3
41
41
  import sys
42
42
  import time
43
43
  import unicodedata
44
+ import urllib.request
44
45
  import uuid
45
46
  from contextlib import contextmanager
46
47
  from datetime import datetime, timedelta, timezone
@@ -52,6 +53,7 @@ from fastapi import FastAPI, Header, HTTPException, Query, Request
52
53
  from fastapi.middleware.cors import CORSMiddleware
53
54
  from fastapi.responses import JSONResponse, Response
54
55
  from pydantic import BaseModel, Field
56
+ from starlette.concurrency import run_in_threadpool
55
57
 
56
58
  from .feature_registry import aggregate_features, apply_allowlist, apply_caps
57
59
 
@@ -71,6 +73,12 @@ SCHEMA_PATH = os.environ.get(
71
73
  )
72
74
  PORT = int(os.environ.get("PORT", "8788"))
73
75
 
76
+ # Gateway-held gbrain proxy token (POST /brain/query). Cells authenticate to
77
+ # THIS gateway with their mesh token; the gateway then presents ITS OWN
78
+ # gbrain token upstream so cells never need a separate gbrain_ credential.
79
+ GATEWAY_GBRAIN_URL = os.environ.get("GATEWAY_GBRAIN_URL", "http://100.107.222.72:8792/mcp")
80
+ GATEWAY_GBRAIN_TOKEN = os.environ.get("GATEWAY_GBRAIN_TOKEN", "")
81
+
74
82
  # B1 Meta-Edge identity (META_EDGE_IDENTITY_CONTRACT.md). Meta-Edge SSO ISSUES
75
83
  # RS256 JWTs; the gateway only ever TRUSTS them with Meta-Edge's PUBLIC key (it
76
84
  # can never forge one — the identity↔relay asymmetry). All three are read at
@@ -4196,6 +4204,59 @@ async def council_reclaim_stuck(req: CouncilReclaimStuckRequest,
4196
4204
  }
4197
4205
 
4198
4206
 
4207
+ # =====================================================================
4208
+ # BRAIN PROXY (POST /brain/query)
4209
+ # =====================================================================
4210
+
4211
+ def _brain_query_upstream(question: str, limit: int) -> list:
4212
+ """Proxy a READ-ONLY gbrain query. Builds the MCP `query` call itself (never
4213
+ write/admin), POSTs to GATEWAY_GBRAIN_URL with the gateway-held token, returns
4214
+ the chunk array. Raises on any upstream/parse failure (caller maps to 502)."""
4215
+ body = {"jsonrpc": "2.0", "id": 1, "method": "tools/call",
4216
+ "params": {"name": "query",
4217
+ "arguments": {"query": question, "limit": limit, "expand": False}}}
4218
+ req = urllib.request.Request(
4219
+ GATEWAY_GBRAIN_URL, data=json.dumps(body).encode("utf-8"), method="POST")
4220
+ req.add_header("Content-Type", "application/json")
4221
+ req.add_header("Accept", "application/json, text/event-stream")
4222
+ req.add_header("Authorization", f"Bearer {GATEWAY_GBRAIN_TOKEN}")
4223
+ with urllib.request.urlopen(req, timeout=30) as resp: # noqa: S310 — fixed tailnet URL
4224
+ raw = resp.read().decode("utf-8")
4225
+ payload = raw
4226
+ if "data:" in raw:
4227
+ for line in raw.splitlines():
4228
+ s = line.strip()
4229
+ if s.startswith("data:"):
4230
+ payload = s[len("data:"):].strip()
4231
+ break
4232
+ doc = json.loads(payload)
4233
+ parsed = json.loads(doc["result"]["content"][0]["text"])
4234
+ return parsed if isinstance(parsed, list) else []
4235
+
4236
+
4237
+ class BrainQueryRequest(BaseModel):
4238
+ query: str
4239
+ limit: int = 8
4240
+
4241
+
4242
+ @app.post("/brain/query")
4243
+ async def brain_query(req: BrainQueryRequest,
4244
+ authorization: Optional[str] = Header(None)) -> dict:
4245
+ auth = _authorize(authorization) # 401 on bad mesh token
4246
+ if not GATEWAY_GBRAIN_TOKEN:
4247
+ raise HTTPException(503, "brain proxy not configured")
4248
+ if not req.query.strip():
4249
+ raise HTTPException(400, "query is required")
4250
+ try:
4251
+ chunks = await run_in_threadpool(_brain_query_upstream, req.query, req.limit)
4252
+ except Exception as e: # fail loud — never a swallowed empty result
4253
+ log.warning("brain proxy upstream error for peer=%s: %s", auth.peer, e)
4254
+ raise HTTPException(502, "brain upstream error")
4255
+ log.info("brain query peer=%s regime=%s limit=%d chunks=%d",
4256
+ auth.peer, auth.regime, req.limit, len(chunks))
4257
+ return {"chunks": chunks}
4258
+
4259
+
4199
4260
  # =====================================================================
4200
4261
  # STARTUP
4201
4262
  # =====================================================================
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swarph-cli
3
- Version: 0.25.0
3
+ Version: 0.26.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`, session `import`, and a stranded-session `watchdog`.
5
5
  Author: Pierre Samson, Claude Opus
6
6
  License: MIT
@@ -105,6 +105,7 @@ tests/test_compress_verify.py
105
105
  tests/test_daemon_command.py
106
106
  tests/test_event_emit.py
107
107
  tests/test_feature_to_uri.py
108
+ tests/test_gateway_brain.py
108
109
  tests/test_gateway_client.py
109
110
  tests/test_gateway_command.py
110
111
  tests/test_highlight_command.py
@@ -13,6 +13,13 @@ from unittest.mock import patch
13
13
  from swarph_cli.commands import brain_ask as ba
14
14
 
15
15
 
16
+ # --- version ---------------------------------------------------------------
17
+
18
+ def test_version_is_0_26_0():
19
+ import swarph_cli
20
+ assert swarph_cli.__version__ == "0.26.0"
21
+
22
+
16
23
  # --- request / response plumbing -------------------------------------------
17
24
 
18
25
  def test_build_query_request_shape():
@@ -122,3 +129,55 @@ def test_resolve_endpoint_default_is_localhost(monkeypatch):
122
129
  monkeypatch.delenv("GBRAIN_MCP_URL", raising=False)
123
130
  monkeypatch.delenv("SWARPH_BRAIN_MCP", raising=False)
124
131
  assert ba._resolve_endpoint() == "http://127.0.0.1:8792/mcp"
132
+
133
+
134
+ # --- SWARPH_BRAIN_GATEWAY client path (Task 2) ------------------------------
135
+
136
+ def test_gateway_query_posts_brain_query_with_peer_token(monkeypatch, tmp_path):
137
+ from swarph_cli.commands import brain_ask
138
+ captured = {}
139
+ def fake_http_post(url, body, token, accept="application/json, text/event-stream", timeout=30):
140
+ captured.update(url=url, body=body, token=token)
141
+ import json as _j
142
+ return _j.dumps({"chunks": [{"slug": "s1", "chunk_text": "x"}]})
143
+ monkeypatch.setattr(brain_ask, "_http_post", fake_http_post)
144
+ out = brain_ask._gateway_query("http://gw:8788", "peer_tok_123", "hello", 5)
145
+ assert out == [{"slug": "s1", "chunk_text": "x"}]
146
+ assert captured["url"] == "http://gw:8788/brain/query"
147
+ assert captured["body"] == {"query": "hello", "limit": 5}
148
+ assert captured["token"] == "peer_tok_123"
149
+
150
+
151
+ def test_run_brain_ask_uses_gateway_when_env_set(monkeypatch, tmp_path, capsys):
152
+ from swarph_cli.commands import brain_ask
153
+ # peer token on disk for _self_name()
154
+ cfg = tmp_path / ".config" / "swarph"
155
+ cfg.mkdir(parents=True)
156
+ (cfg / "wlc.peer_token").write_text("peer_tok_xyz", encoding="utf-8")
157
+ monkeypatch.setattr(brain_ask.Path, "home", classmethod(lambda cls: tmp_path))
158
+ monkeypatch.setenv("SWARPH_SELF", "wlc")
159
+ monkeypatch.setenv("SWARPH_BRAIN_GATEWAY", "http://gw:8788")
160
+ seen = {}
161
+ def fake_gateway_query(gw, tok, q, n):
162
+ seen.update(gw=gw, tok=tok, q=q)
163
+ return [{"slug": "s1", "chunk_text": "hit"}]
164
+ monkeypatch.setattr(brain_ask, "_gateway_query", fake_gateway_query)
165
+ rc = brain_ask.run_brain_ask(["--no-synth", "what", "is", "up"])
166
+ assert rc == 0
167
+ assert seen["gw"] == "http://gw:8788" and seen["tok"] == "peer_tok_xyz"
168
+ assert "s1" in capsys.readouterr().out
169
+
170
+
171
+ def test_run_brain_ask_direct_path_unchanged_when_gateway_unset(monkeypatch, capsys):
172
+ from swarph_cli.commands import brain_ask
173
+ monkeypatch.delenv("SWARPH_BRAIN_GATEWAY", raising=False)
174
+ monkeypatch.setenv("GBRAIN_TOKEN", "gbrain_direct")
175
+ called = {}
176
+ def fake_mcp(url, tok, q, n):
177
+ called["hit"] = (url, tok)
178
+ return [{"slug": "d"}]
179
+ monkeypatch.setattr(brain_ask, "_mcp_query", fake_mcp)
180
+ rc = brain_ask.run_brain_ask(["--no-synth", "hello"])
181
+ assert rc == 0
182
+ assert called["hit"][1] == "gbrain_direct" # direct path used the gbrain token
183
+ assert "d" in capsys.readouterr().out
@@ -0,0 +1,104 @@
1
+ import json
2
+ import os
3
+ import importlib
4
+ import tempfile
5
+ import pytest
6
+ from types import SimpleNamespace
7
+
8
+ # The gateway server stack is an optional extra ([gateway]); its module top
9
+ # does `import jwt` (PyJWT) + fastapi. Skip the whole file when those aren't
10
+ # installed (e.g. Windows CI, which doesn't install the gateway extra) — same
11
+ # guard the sibling gateway test tests/test_meta_edge_identity.py uses.
12
+ pytest.importorskip("fastapi")
13
+ pytest.importorskip("jwt")
14
+
15
+
16
+ def _load_app(monkeypatch, *, gbrain_token="gbrain_hostheld", auth="tok_shared"):
17
+ monkeypatch.setenv("MESH_GATEWAY_TOKEN", auth)
18
+ monkeypatch.setenv("GATEWAY_GBRAIN_TOKEN", gbrain_token)
19
+ monkeypatch.setenv("GATEWAY_GBRAIN_URL", "http://127.0.0.1:8792/mcp")
20
+ # NOTE: ":memory:" (as written in the task brief) is incompatible with this
21
+ # module's _conn()/_init_db(), which open a FRESH sqlite3.connect() per call
22
+ # with no shared cache — each ":memory:" connection is an independent,
23
+ # isolated DB, so the schema built by _init_db()'s first connection is gone
24
+ # by its second. That fails at import time, before any endpoint code runs,
25
+ # for every one of the 6 tests here — not the brief's expected 404/
26
+ # AttributeError RED. Using a real temp-file path (matching the existing
27
+ # pattern in tests/test_meta_edge_identity.py) fixes the scaffold without
28
+ # touching production DB code, and restores the brief's intended RED.
29
+ monkeypatch.setenv("MESH_DB_PATH", os.path.join(tempfile.mkdtemp(), "mesh.db"))
30
+ from swarph_cli.gateway import server
31
+ importlib.reload(server) # re-read module-top env config
32
+ return server
33
+
34
+
35
+ def _sse(chunks):
36
+ inner = json.dumps(chunks)
37
+ env = {"jsonrpc": "2.0", "id": 1, "result": {"content": [{"type": "text", "text": inner}]}}
38
+ return "event: message\ndata: " + json.dumps(env) + "\n\n"
39
+
40
+
41
+ def _client(server):
42
+ from fastapi.testclient import TestClient
43
+ return TestClient(server.app)
44
+
45
+
46
+ def test_brain_query_authenticates_and_returns_chunks(monkeypatch):
47
+ server = _load_app(monkeypatch)
48
+ chunks = [{"slug": "s1", "title": "T", "chunk_text": "hello", "score": 0.9}]
49
+ monkeypatch.setattr(server, "_brain_query_upstream", lambda q, n: chunks)
50
+ r = _client(server).post("/brain/query", json={"query": "hi", "limit": 5},
51
+ headers={"Authorization": "Bearer tok_shared"})
52
+ assert r.status_code == 200
53
+ assert r.json() == {"chunks": chunks}
54
+
55
+
56
+ def test_brain_query_bad_token_401(monkeypatch):
57
+ server = _load_app(monkeypatch)
58
+ monkeypatch.setattr(server, "_brain_query_upstream", lambda q, n: [])
59
+ r = _client(server).post("/brain/query", json={"query": "hi"},
60
+ headers={"Authorization": "Bearer wrong"})
61
+ assert r.status_code == 401
62
+
63
+
64
+ def test_brain_query_unconfigured_503(monkeypatch):
65
+ server = _load_app(monkeypatch, gbrain_token="") # GATEWAY_GBRAIN_TOKEN unset
66
+ r = _client(server).post("/brain/query", json={"query": "hi"},
67
+ headers={"Authorization": "Bearer tok_shared"})
68
+ assert r.status_code == 503
69
+
70
+
71
+ def test_brain_query_empty_query_400(monkeypatch):
72
+ server = _load_app(monkeypatch)
73
+ monkeypatch.setattr(server, "_brain_query_upstream", lambda q, n: [])
74
+ r = _client(server).post("/brain/query", json={"query": " "},
75
+ headers={"Authorization": "Bearer tok_shared"})
76
+ assert r.status_code == 400
77
+
78
+
79
+ def test_brain_query_upstream_error_502(monkeypatch):
80
+ server = _load_app(monkeypatch)
81
+ def boom(q, n): raise RuntimeError("gbrain down")
82
+ monkeypatch.setattr(server, "_brain_query_upstream", boom)
83
+ r = _client(server).post("/brain/query", json={"query": "hi"},
84
+ headers={"Authorization": "Bearer tok_shared"})
85
+ assert r.status_code == 502
86
+
87
+
88
+ def test_upstream_helper_is_read_only_by_construction(monkeypatch):
89
+ """The MCP body the proxy sends upstream ALWAYS has params.name == 'query'."""
90
+ server = _load_app(monkeypatch)
91
+ captured = {}
92
+ class FakeResp:
93
+ def read(self): return _sse([{"slug": "s"}]).encode()
94
+ def __enter__(self): return self
95
+ def __exit__(self, *a): return False
96
+ def fake_urlopen(req, timeout=None):
97
+ captured["body"] = json.loads(req.data.decode())
98
+ captured["auth"] = req.get_header("Authorization")
99
+ return FakeResp()
100
+ monkeypatch.setattr(server.urllib.request, "urlopen", fake_urlopen)
101
+ out = server._brain_query_upstream("anything", 8)
102
+ assert out == [{"slug": "s"}]
103
+ assert captured["body"]["params"]["name"] == "query" # read-only lock
104
+ assert captured["auth"] == "Bearer gbrain_hostheld" # gateway's held token
@@ -1041,6 +1041,6 @@ def test_parser_defaults_process_name_claude_liveness_cmd_none():
1041
1041
  assert ns.liveness_cmd is None
1042
1042
 
1043
1043
 
1044
- def test_version_is_0_25_0():
1044
+ def test_version_is_0_26_0():
1045
1045
  import swarph_cli
1046
- assert swarph_cli.__version__ == "0.25.0"
1046
+ assert swarph_cli.__version__ == "0.26.0"
File without changes
File without changes
File without changes