swarph-cli 0.22.0__tar.gz → 0.23.1__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.22.0/src/swarph_cli.egg-info → swarph_cli-0.23.1}/PKG-INFO +1 -1
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/pyproject.toml +1 -1
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/__init__.py +1 -1
- swarph_cli-0.23.1/src/swarph_cli/chain_token.py +42 -0
- swarph_cli-0.23.1/src/swarph_cli/commands/event.py +119 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/main.py +1 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1/src/swarph_cli.egg-info}/PKG-INFO +1 -1
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli.egg-info/SOURCES.txt +4 -0
- swarph_cli-0.23.1/tests/test_chain_token.py +34 -0
- swarph_cli-0.23.1/tests/test_event_emit.py +36 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/LICENSE +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/README.md +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/setup.cfg +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/caller.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/capture/__init__.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/capture/harden.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/capture/lineage.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/capture/liveness.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/capture/manifest.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/capture/paths.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/capture/verify.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/cell.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/commands/__init__.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/commands/_gateway_client.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/commands/add.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/commands/brain.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/commands/brain_ask.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/commands/cell.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/commands/channel.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/commands/chat.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/commands/compress.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/commands/daemon.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/commands/gateway.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/commands/highlight.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/commands/hook_output.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/commands/hooks.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/commands/import_session.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/commands/init.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/commands/install_hook.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/commands/install_multiplexer.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/commands/lane.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/commands/mcp_server.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/commands/memory_sync.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/commands/mesh.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/commands/onboard.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/commands/protocol_handler.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/commands/ratify.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/commands/schedule.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/commands/security.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/commands/service.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/commands/spawn.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/commands/watchdog.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/compress/__init__.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/compress/levers.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/compress/marker.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/compress/verify.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/gateway/__init__.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/gateway/feature_registry.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/gateway/lanes_control.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/gateway/schema.sql +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/gateway/server.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/gateway/services_control.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/multiplexer.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/parsers/__init__.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/parsers/claude.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/service/__init__.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/service/app.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/service/providers.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli.egg-info/entry_points.txt +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli.egg-info/requires.txt +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/src/swarph_cli.egg-info/top_level.txt +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_add_security_gate.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_artifact_add.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_artifact_hash.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_artifact_lib.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_artifact_mcp.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_artifact_skill.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_artifact_tool.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_artifact_uri.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_brain_ask_command.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_brain_command.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_caller_meta_guard.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_capture_lineage.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_capture_liveness.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_capture_manifest.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_capture_paths.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_capture_security.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_cell_command_dispatch.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_cell_harden.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_cell_loader.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_cell_verify.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_channel_command.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_chat_command.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_claude_parser.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_claude_tmux_template.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_compress_caller_convention.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_compress_command.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_compress_levers.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_compress_marker.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_compress_verify.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_daemon_command.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_feature_to_uri.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_gateway_client.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_gateway_command.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_highlight_command.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_hook_output.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_hooks_add.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_hooks_bundle.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_hooks_init.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_hooks_lifecycle.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_hooks_merge.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_import_command.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_init_command.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_install_hook.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_install_multiplexer_command.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_lane_command.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_main.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_mcp_server.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_memory_sync.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_mesh_command.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_mesh_sidecar.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_meta_edge_identity.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_multiplexer.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_onboard_command.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_protocol_handler.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_ratify_command.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_repl_caller_convention.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_schedule_command.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_security.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_service_app.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_service_command.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_service_providers.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_session_path_role_gate.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_smoke_chat.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_smoke_one_shot.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_smoke_phase_5_5.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_spawn_command.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_spawn_live_pin.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_spawn_mitosis_lineage.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_spawn_tmux_session.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_spawn_windows_relaunch.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_version_consistency.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_watchdog.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_watchdog_dm_wake.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_watchdog_dm_wake_cooldown.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_watchdog_dm_wake_wiring.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/tests/test_watchdog_model_rung.py +0 -0
- {swarph_cli-0.22.0 → swarph_cli-0.23.1}/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.23.1
|
|
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.23.1"
|
|
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" }
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Producer-signed chain-token for event-chaining (spec §5a). Compact HMAC blob the
|
|
2
|
+
producer signs + verifies; the cell only passes it through opaquely (can't forge it)."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import base64, hashlib, hmac, json
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _b64(b: bytes) -> str:
|
|
9
|
+
return base64.urlsafe_b64encode(b).decode().rstrip("=")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _unb64(s: str) -> bytes:
|
|
13
|
+
return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def sign_chain_token(secret: str, chain_id: str, depth: int, jti: str) -> str:
|
|
17
|
+
payload = json.dumps({"chain_id": chain_id, "depth": int(depth), "jti": jti},
|
|
18
|
+
separators=(",", ":"), sort_keys=True).encode()
|
|
19
|
+
sig = hmac.new(secret.encode(), payload, hashlib.sha256).digest()
|
|
20
|
+
return f"{_b64(payload)}.{_b64(sig)}"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def verify_chain_token(secret: str, token: str) -> Optional[dict]:
|
|
24
|
+
# token is opaque untrusted input (network / mesh DM); a non-str value has no
|
|
25
|
+
# .split and would AttributeError past the except below. Guard explicitly.
|
|
26
|
+
if not isinstance(token, str):
|
|
27
|
+
return None
|
|
28
|
+
try:
|
|
29
|
+
body_s, sig_s = token.split(".", 1)
|
|
30
|
+
payload = _unb64(body_s)
|
|
31
|
+
expected = hmac.new(secret.encode(), payload, hashlib.sha256).digest()
|
|
32
|
+
if not hmac.compare_digest(_unb64(sig_s), expected):
|
|
33
|
+
return None
|
|
34
|
+
d = json.loads(payload)
|
|
35
|
+
if not isinstance(d, dict) or {"chain_id", "depth", "jti"} - d.keys():
|
|
36
|
+
return None
|
|
37
|
+
return {"chain_id": d["chain_id"], "depth": int(d["depth"]), "jti": d["jti"]}
|
|
38
|
+
except (ValueError, TypeError, KeyError, AttributeError, json.JSONDecodeError):
|
|
39
|
+
# the known malformed-input paths (split / b64 / json / int / dict /
|
|
40
|
+
# non-str secret). Fail safe to None. NOT a bare `Exception`: an
|
|
41
|
+
# unexpected error should surface as a bug, not masquerade as "invalid".
|
|
42
|
+
return None
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""``swarph event`` — emit an event to a mesh channel (event-chaining P0).
|
|
2
|
+
|
|
3
|
+
``swarph event emit <event> <payload> [--chain-token <opaque>] [--channel events]``
|
|
4
|
+
posts to a channel via the shipped channel-post client, carrying an ``event`` tag,
|
|
5
|
+
the payload, and any ``--chain-token`` as an OPAQUE passthrough. The cell never
|
|
6
|
+
signs or parses the chain-token — signing/verifying is the producer's authority
|
|
7
|
+
(the P1 gateway guard). See ``swarph_cli.chain_token`` for the producer-side
|
|
8
|
+
sign/verify helpers.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
from ._gateway_client import (
|
|
18
|
+
add_common_args,
|
|
19
|
+
post_json,
|
|
20
|
+
resolve_self_name,
|
|
21
|
+
resolve_token,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
26
|
+
p = argparse.ArgumentParser(
|
|
27
|
+
prog="swarph event",
|
|
28
|
+
description="Emit an event to a mesh channel (opaque chain-token passthrough).",
|
|
29
|
+
)
|
|
30
|
+
sub = p.add_subparsers(dest="command")
|
|
31
|
+
|
|
32
|
+
emit = sub.add_parser("emit", help="emit an event to a channel")
|
|
33
|
+
emit.add_argument("event", help="event name/tag")
|
|
34
|
+
emit.add_argument("payload", help="event payload (opaque string)")
|
|
35
|
+
emit.add_argument(
|
|
36
|
+
"--chain-token",
|
|
37
|
+
default=None,
|
|
38
|
+
help="opaque producer-signed chain token — passed through, never parsed",
|
|
39
|
+
)
|
|
40
|
+
emit.add_argument(
|
|
41
|
+
"--channel",
|
|
42
|
+
default="events",
|
|
43
|
+
help="target channel (default: events)",
|
|
44
|
+
)
|
|
45
|
+
add_common_args(emit)
|
|
46
|
+
return p
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _post_channel(
|
|
50
|
+
*,
|
|
51
|
+
channel: str,
|
|
52
|
+
event: str,
|
|
53
|
+
payload: str,
|
|
54
|
+
chain_token: str | None = None,
|
|
55
|
+
gateway: str | None = None,
|
|
56
|
+
self_name: str | None = None,
|
|
57
|
+
token_file: str | None = None,
|
|
58
|
+
) -> int:
|
|
59
|
+
"""Bridge to the shipped channel-post client (the ``/messages`` channel path
|
|
60
|
+
that ``swarph channel`` / ``swarph mesh`` post through). Wraps the event tag +
|
|
61
|
+
payload + (opaque) chain-token into the post body and returns 0 on a 2xx.
|
|
62
|
+
|
|
63
|
+
NOTE: the plan's seam is ``_post_channel(**kw)``; the gateway's ``post_json``
|
|
64
|
+
takes ``(url, body, token)``, so this function resolves identity/token and
|
|
65
|
+
shapes the body, then delegates to that same shared client — it does NOT
|
|
66
|
+
reinvent the gateway post path. Tests monkeypatch this function directly.
|
|
67
|
+
"""
|
|
68
|
+
name = resolve_self_name(self_name)
|
|
69
|
+
token = resolve_token(name, token_file)
|
|
70
|
+
from .mesh import _DEFAULT_GATEWAY
|
|
71
|
+
|
|
72
|
+
base = (gateway or _DEFAULT_GATEWAY).rstrip("/")
|
|
73
|
+
ev: dict = {"event": event, "payload": payload}
|
|
74
|
+
if chain_token is not None: # carry an explicit "" through; only omit when unset
|
|
75
|
+
ev["chain_token"] = chain_token
|
|
76
|
+
# Sentinel-wrapped so P1's guard can UNAMBIGUOUSLY identify an event post: a
|
|
77
|
+
# normal channel message won't carry the top-level `swarph_event` key, so the
|
|
78
|
+
# guard keys off `content.swarph_event` — never a false-positive on a plain
|
|
79
|
+
# message whose content happens to hold an "event" field. kind stays "fyi"
|
|
80
|
+
# (gateway VALID_KINDS); P1 promotes this to a first-class kind/column when it
|
|
81
|
+
# touches the gateway.
|
|
82
|
+
content: dict = {"swarph_event": ev}
|
|
83
|
+
body = {
|
|
84
|
+
"from_node": name,
|
|
85
|
+
"channel": channel,
|
|
86
|
+
"kind": "fyi",
|
|
87
|
+
"content": json.dumps(content, separators=(",", ":"), sort_keys=True),
|
|
88
|
+
}
|
|
89
|
+
status, resp = post_json(f"{base}/messages", body, token)
|
|
90
|
+
if 200 <= status < 300:
|
|
91
|
+
print(f"emitted event={event} channel={channel} id={resp.get('id')}")
|
|
92
|
+
return 0
|
|
93
|
+
detail = resp.get("detail", "<gateway error>")
|
|
94
|
+
print(f"swarph event emit: gateway {status}: {detail}", file=sys.stderr)
|
|
95
|
+
return 1
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def run_event(argv: list) -> int:
|
|
99
|
+
p = _build_parser()
|
|
100
|
+
args = p.parse_args(argv)
|
|
101
|
+
if args.command is None:
|
|
102
|
+
p.print_help()
|
|
103
|
+
return 0
|
|
104
|
+
try:
|
|
105
|
+
if args.command == "emit":
|
|
106
|
+
return _post_channel(
|
|
107
|
+
channel=args.channel,
|
|
108
|
+
event=args.event,
|
|
109
|
+
payload=args.payload,
|
|
110
|
+
chain_token=args.chain_token,
|
|
111
|
+
gateway=args.gateway,
|
|
112
|
+
self_name=args.self_name,
|
|
113
|
+
token_file=args.token_file,
|
|
114
|
+
)
|
|
115
|
+
except RuntimeError as e:
|
|
116
|
+
print(f"swarph event: {e}", file=sys.stderr)
|
|
117
|
+
return 2
|
|
118
|
+
p.print_help()
|
|
119
|
+
return 0
|
|
@@ -96,6 +96,7 @@ _VERB_HANDLERS: dict[str, str] = {
|
|
|
96
96
|
"gateway": "swarph_cli.commands.gateway.run_gateway",
|
|
97
97
|
"service": "swarph_cli.commands.service.run_service",
|
|
98
98
|
"channel": "swarph_cli.commands.channel.run_channel",
|
|
99
|
+
"event": "swarph_cli.commands.event.run_event",
|
|
99
100
|
"schedule": "swarph_cli.commands.schedule.run_schedule",
|
|
100
101
|
"lane": "swarph_cli.commands.lane.run_lane",
|
|
101
102
|
"install-multiplexer": "swarph_cli.commands.install_multiplexer.run_install_multiplexer",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: swarph-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.23.1
|
|
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,6 +4,7 @@ pyproject.toml
|
|
|
4
4
|
src/swarph_cli/__init__.py
|
|
5
5
|
src/swarph_cli/caller.py
|
|
6
6
|
src/swarph_cli/cell.py
|
|
7
|
+
src/swarph_cli/chain_token.py
|
|
7
8
|
src/swarph_cli/main.py
|
|
8
9
|
src/swarph_cli/multiplexer.py
|
|
9
10
|
src/swarph_cli.egg-info/PKG-INFO
|
|
@@ -29,6 +30,7 @@ src/swarph_cli/commands/channel.py
|
|
|
29
30
|
src/swarph_cli/commands/chat.py
|
|
30
31
|
src/swarph_cli/commands/compress.py
|
|
31
32
|
src/swarph_cli/commands/daemon.py
|
|
33
|
+
src/swarph_cli/commands/event.py
|
|
32
34
|
src/swarph_cli/commands/gateway.py
|
|
33
35
|
src/swarph_cli/commands/highlight.py
|
|
34
36
|
src/swarph_cli/commands/hook_output.py
|
|
@@ -87,6 +89,7 @@ tests/test_cell_command_dispatch.py
|
|
|
87
89
|
tests/test_cell_harden.py
|
|
88
90
|
tests/test_cell_loader.py
|
|
89
91
|
tests/test_cell_verify.py
|
|
92
|
+
tests/test_chain_token.py
|
|
90
93
|
tests/test_channel_command.py
|
|
91
94
|
tests/test_chat_command.py
|
|
92
95
|
tests/test_claude_parser.py
|
|
@@ -97,6 +100,7 @@ tests/test_compress_levers.py
|
|
|
97
100
|
tests/test_compress_marker.py
|
|
98
101
|
tests/test_compress_verify.py
|
|
99
102
|
tests/test_daemon_command.py
|
|
103
|
+
tests/test_event_emit.py
|
|
100
104
|
tests/test_feature_to_uri.py
|
|
101
105
|
tests/test_gateway_client.py
|
|
102
106
|
tests/test_gateway_command.py
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from swarph_cli.chain_token import sign_chain_token, verify_chain_token
|
|
3
|
+
|
|
4
|
+
SECRET = "producer-secret-xyz"
|
|
5
|
+
|
|
6
|
+
def test_sign_verify_roundtrip():
|
|
7
|
+
t = sign_chain_token(SECRET, "chain-1", 3, "jti-abc")
|
|
8
|
+
assert verify_chain_token(SECRET, t) == {"chain_id": "chain-1", "depth": 3, "jti": "jti-abc"}
|
|
9
|
+
|
|
10
|
+
def test_tampered_payload_rejected():
|
|
11
|
+
t = sign_chain_token(SECRET, "chain-1", 0, "jti-abc")
|
|
12
|
+
_, sig = t.split(".")
|
|
13
|
+
import base64, json
|
|
14
|
+
# CANONICAL re-encode (same separators/sort_keys the signer uses) with ONLY depth
|
|
15
|
+
# changed → isolates that the HMAC catches the VALUE change, not a formatting diff.
|
|
16
|
+
forged = json.dumps({"chain_id": "chain-1", "depth": 99, "jti": "jti-abc"},
|
|
17
|
+
separators=(",", ":"), sort_keys=True).encode()
|
|
18
|
+
bad = base64.urlsafe_b64encode(forged).decode().rstrip("=") + "." + sig
|
|
19
|
+
assert verify_chain_token(SECRET, bad) is None # depth 0→99, canonical form, sig mismatch
|
|
20
|
+
|
|
21
|
+
def test_wrong_secret_rejected():
|
|
22
|
+
t = sign_chain_token(SECRET, "c", 1, "j")
|
|
23
|
+
assert verify_chain_token("other-secret", t) is None
|
|
24
|
+
|
|
25
|
+
def test_malformed_never_raises():
|
|
26
|
+
for bad in ["", "nodot", "a.b.c", "!!.??", "x."]:
|
|
27
|
+
assert verify_chain_token(SECRET, bad) is None
|
|
28
|
+
|
|
29
|
+
def test_non_str_token_fails_safe_not_raises():
|
|
30
|
+
# opaque tokens arrive from untrusted input (network / mesh DM); a non-str
|
|
31
|
+
# value (None, bytes, int) must fail safe to None, never AttributeError out
|
|
32
|
+
# of the caller. Regression for the gateway-copy fix that lagged the CLI.
|
|
33
|
+
for bad in [None, 123, b"a.b", ["a", "b"], {"chain_id": "x"}]:
|
|
34
|
+
assert verify_chain_token(SECRET, bad) is None
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
def test_event_emit_posts_with_tag_and_token(monkeypatch):
|
|
2
|
+
from swarph_cli.commands import event as evmod
|
|
3
|
+
captured = {}
|
|
4
|
+
monkeypatch.setattr(evmod, "_post_channel", lambda **kw: captured.update(kw) or 0)
|
|
5
|
+
rc = evmod.run_event(["emit", "stage-done", "the output", "--chain-token", "TOK", "--channel", "events"])
|
|
6
|
+
assert rc == 0
|
|
7
|
+
assert captured["channel"] == "events"
|
|
8
|
+
assert captured["event"] == "stage-done"
|
|
9
|
+
assert captured["payload"] == "the output"
|
|
10
|
+
assert captured["chain_token"] == "TOK"
|
|
11
|
+
|
|
12
|
+
def test_event_emit_no_token(monkeypatch):
|
|
13
|
+
from swarph_cli.commands import event as evmod
|
|
14
|
+
captured = {}
|
|
15
|
+
monkeypatch.setattr(evmod, "_post_channel", lambda **kw: captured.update(kw) or 0)
|
|
16
|
+
evmod.run_event(["emit", "e", "p"])
|
|
17
|
+
assert captured.get("chain_token") is None # argparse default=None, not ""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_event_emit_real_envelope_is_sentinel_wrapped(monkeypatch):
|
|
21
|
+
# exercise the REAL _post_channel (not the seam) to prove the content envelope is
|
|
22
|
+
# sentinel-wrapped so P1's guard identifies events unambiguously (a plain channel
|
|
23
|
+
# message won't carry the top-level `swarph_event` key).
|
|
24
|
+
import json
|
|
25
|
+
from swarph_cli.commands import event as evmod
|
|
26
|
+
monkeypatch.setattr(evmod, "resolve_self_name", lambda *a, **k: "lab-ovh")
|
|
27
|
+
monkeypatch.setattr(evmod, "resolve_token", lambda *a, **k: "tok")
|
|
28
|
+
captured = {}
|
|
29
|
+
monkeypatch.setattr(evmod, "post_json",
|
|
30
|
+
lambda url, body, token: (captured.update(body=body) or (200, {"id": 1})))
|
|
31
|
+
rc = evmod.run_event(["emit", "stage-done", "out", "--chain-token", "TOK"])
|
|
32
|
+
assert rc == 0
|
|
33
|
+
content = json.loads(captured["body"]["content"])
|
|
34
|
+
assert "swarph_event" in content
|
|
35
|
+
assert content["swarph_event"] == {"event": "stage-done", "payload": "out", "chain_token": "TOK"}
|
|
36
|
+
assert captured["body"]["kind"] == "fyi" # VALID_KINDS-safe; P1 promotes to first-class
|
|
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
|