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.
- {swarph_cli-0.25.0/src/swarph_cli.egg-info → swarph_cli-0.26.0}/PKG-INFO +1 -1
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/pyproject.toml +1 -1
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/__init__.py +1 -1
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/brain_ask.py +42 -13
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/gateway/server.py +61 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0/src/swarph_cli.egg-info}/PKG-INFO +1 -1
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli.egg-info/SOURCES.txt +1 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_brain_ask_command.py +59 -0
- swarph_cli-0.26.0/tests/test_gateway_brain.py +104 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_watchdog.py +2 -2
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/LICENSE +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/README.md +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/setup.cfg +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/caller.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/capture/__init__.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/capture/harden.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/capture/lineage.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/capture/liveness.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/capture/manifest.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/capture/paths.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/capture/verify.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/cell.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/chain_token.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/__init__.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/_gateway_client.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/add.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/brain.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/cell.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/channel.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/chat.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/codegraph.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/compress.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/daemon.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/event.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/gateway.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/highlight.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/hook_output.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/hooks.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/import_session.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/init.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/install_hook.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/install_multiplexer.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/lane.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/mcp_server.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/memory_sync.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/mesh.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/onboard.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/protocol_handler.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/ratify.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/schedule.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/security.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/service.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/spawn.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/commands/watchdog.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/compress/__init__.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/compress/levers.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/compress/marker.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/compress/verify.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/gateway/__init__.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/gateway/feature_registry.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/gateway/lanes_control.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/gateway/schema.sql +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/gateway/services_control.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/main.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/multiplexer.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/parsers/__init__.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/parsers/claude.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/service/__init__.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/service/app.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/service/providers.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli.egg-info/entry_points.txt +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli.egg-info/requires.txt +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/src/swarph_cli.egg-info/top_level.txt +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_add_security_gate.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_artifact_add.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_artifact_hash.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_artifact_lib.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_artifact_mcp.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_artifact_skill.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_artifact_tool.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_artifact_uri.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_brain_command.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_caller_meta_guard.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_capture_lineage.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_capture_liveness.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_capture_manifest.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_capture_paths.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_capture_security.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_cell_command_dispatch.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_cell_harden.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_cell_loader.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_cell_verify.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_chain_token.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_channel_command.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_chat_command.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_claude_parser.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_claude_tmux_template.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_codegraph_command.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_codegraph_mcp.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_compress_caller_convention.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_compress_command.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_compress_levers.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_compress_marker.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_compress_verify.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_daemon_command.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_event_emit.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_feature_to_uri.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_gateway_client.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_gateway_command.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_highlight_command.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_hook_output.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_hooks_add.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_hooks_bundle.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_hooks_init.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_hooks_lifecycle.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_hooks_merge.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_import_command.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_init_command.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_install_hook.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_install_multiplexer_command.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_lane_command.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_main.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_mcp_server.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_memory_sync.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_mesh_command.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_mesh_sidecar.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_meta_edge_identity.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_multiplexer.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_onboard_command.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_protocol_handler.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_ratify_command.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_repl_caller_convention.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_schedule_command.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_security.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_service_app.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_service_command.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_service_providers.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_session_path_role_gate.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_smoke_chat.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_smoke_one_shot.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_smoke_phase_5_5.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_spawn_command.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_spawn_live_pin.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_spawn_mitosis_lineage.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_spawn_tmux_session.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_spawn_windows_relaunch.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_version_consistency.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_watchdog_dm_wake.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_watchdog_dm_wake_cooldown.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_watchdog_dm_wake_wiring.py +0 -0
- {swarph_cli-0.25.0 → swarph_cli-0.26.0}/tests/test_watchdog_model_rung.py +0 -0
- {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.
|
|
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.
|
|
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" }
|
|
@@ -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
|
-
|
|
162
|
-
if
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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.
|
|
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
|
|
1044
|
+
def test_version_is_0_26_0():
|
|
1045
1045
|
import swarph_cli
|
|
1046
|
-
assert swarph_cli.__version__ == "0.
|
|
1046
|
+
assert swarph_cli.__version__ == "0.26.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|