swarph-cli 0.22.0__tar.gz → 0.23.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 (152) hide show
  1. {swarph_cli-0.22.0/src/swarph_cli.egg-info → swarph_cli-0.23.0}/PKG-INFO +1 -1
  2. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/pyproject.toml +1 -1
  3. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/__init__.py +1 -1
  4. swarph_cli-0.23.0/src/swarph_cli/chain_token.py +38 -0
  5. swarph_cli-0.23.0/src/swarph_cli/commands/event.py +119 -0
  6. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/main.py +1 -0
  7. {swarph_cli-0.22.0 → swarph_cli-0.23.0/src/swarph_cli.egg-info}/PKG-INFO +1 -1
  8. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli.egg-info/SOURCES.txt +4 -0
  9. swarph_cli-0.23.0/tests/test_chain_token.py +27 -0
  10. swarph_cli-0.23.0/tests/test_event_emit.py +36 -0
  11. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/LICENSE +0 -0
  12. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/README.md +0 -0
  13. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/setup.cfg +0 -0
  14. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/caller.py +0 -0
  15. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/capture/__init__.py +0 -0
  16. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/capture/harden.py +0 -0
  17. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/capture/lineage.py +0 -0
  18. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/capture/liveness.py +0 -0
  19. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/capture/manifest.py +0 -0
  20. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/capture/paths.py +0 -0
  21. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/capture/verify.py +0 -0
  22. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/cell.py +0 -0
  23. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/commands/__init__.py +0 -0
  24. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/commands/_gateway_client.py +0 -0
  25. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/commands/add.py +0 -0
  26. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/commands/brain.py +0 -0
  27. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/commands/brain_ask.py +0 -0
  28. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/commands/cell.py +0 -0
  29. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/commands/channel.py +0 -0
  30. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/commands/chat.py +0 -0
  31. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/commands/compress.py +0 -0
  32. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/commands/daemon.py +0 -0
  33. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/commands/gateway.py +0 -0
  34. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/commands/highlight.py +0 -0
  35. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/commands/hook_output.py +0 -0
  36. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/commands/hooks.py +0 -0
  37. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/commands/import_session.py +0 -0
  38. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/commands/init.py +0 -0
  39. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/commands/install_hook.py +0 -0
  40. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/commands/install_multiplexer.py +0 -0
  41. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/commands/lane.py +0 -0
  42. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/commands/mcp_server.py +0 -0
  43. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/commands/memory_sync.py +0 -0
  44. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/commands/mesh.py +0 -0
  45. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/commands/onboard.py +0 -0
  46. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/commands/protocol_handler.py +0 -0
  47. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/commands/ratify.py +0 -0
  48. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/commands/schedule.py +0 -0
  49. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/commands/security.py +0 -0
  50. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/commands/service.py +0 -0
  51. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/commands/spawn.py +0 -0
  52. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/commands/watchdog.py +0 -0
  53. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/compress/__init__.py +0 -0
  54. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/compress/levers.py +0 -0
  55. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/compress/marker.py +0 -0
  56. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/compress/verify.py +0 -0
  57. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/gateway/__init__.py +0 -0
  58. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/gateway/feature_registry.py +0 -0
  59. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/gateway/lanes_control.py +0 -0
  60. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/gateway/schema.sql +0 -0
  61. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/gateway/server.py +0 -0
  62. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/gateway/services_control.py +0 -0
  63. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/multiplexer.py +0 -0
  64. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/parsers/__init__.py +0 -0
  65. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/parsers/claude.py +0 -0
  66. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/service/__init__.py +0 -0
  67. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/service/app.py +0 -0
  68. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/service/providers.py +0 -0
  69. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
  70. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
  71. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
  72. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
  73. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli.egg-info/entry_points.txt +0 -0
  74. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli.egg-info/requires.txt +0 -0
  75. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/src/swarph_cli.egg-info/top_level.txt +0 -0
  76. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_add_security_gate.py +0 -0
  77. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_artifact_add.py +0 -0
  78. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_artifact_hash.py +0 -0
  79. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_artifact_lib.py +0 -0
  80. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_artifact_mcp.py +0 -0
  81. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_artifact_skill.py +0 -0
  82. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_artifact_tool.py +0 -0
  83. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_artifact_uri.py +0 -0
  84. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_brain_ask_command.py +0 -0
  85. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_brain_command.py +0 -0
  86. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_caller_meta_guard.py +0 -0
  87. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_capture_lineage.py +0 -0
  88. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_capture_liveness.py +0 -0
  89. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_capture_manifest.py +0 -0
  90. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_capture_paths.py +0 -0
  91. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_capture_security.py +0 -0
  92. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_cell_command_dispatch.py +0 -0
  93. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_cell_harden.py +0 -0
  94. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_cell_loader.py +0 -0
  95. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_cell_verify.py +0 -0
  96. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_channel_command.py +0 -0
  97. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_chat_command.py +0 -0
  98. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_claude_parser.py +0 -0
  99. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_claude_tmux_template.py +0 -0
  100. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_compress_caller_convention.py +0 -0
  101. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_compress_command.py +0 -0
  102. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_compress_levers.py +0 -0
  103. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_compress_marker.py +0 -0
  104. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_compress_verify.py +0 -0
  105. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_daemon_command.py +0 -0
  106. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_feature_to_uri.py +0 -0
  107. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_gateway_client.py +0 -0
  108. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_gateway_command.py +0 -0
  109. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_highlight_command.py +0 -0
  110. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_hook_output.py +0 -0
  111. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_hooks_add.py +0 -0
  112. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_hooks_bundle.py +0 -0
  113. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_hooks_init.py +0 -0
  114. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_hooks_lifecycle.py +0 -0
  115. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_hooks_merge.py +0 -0
  116. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_import_command.py +0 -0
  117. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_init_command.py +0 -0
  118. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_install_hook.py +0 -0
  119. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_install_multiplexer_command.py +0 -0
  120. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_lane_command.py +0 -0
  121. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_main.py +0 -0
  122. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_mcp_server.py +0 -0
  123. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_memory_sync.py +0 -0
  124. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_mesh_command.py +0 -0
  125. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_mesh_sidecar.py +0 -0
  126. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_meta_edge_identity.py +0 -0
  127. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_multiplexer.py +0 -0
  128. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_onboard_command.py +0 -0
  129. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_protocol_handler.py +0 -0
  130. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_ratify_command.py +0 -0
  131. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_repl_caller_convention.py +0 -0
  132. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_schedule_command.py +0 -0
  133. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_security.py +0 -0
  134. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_service_app.py +0 -0
  135. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_service_command.py +0 -0
  136. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_service_providers.py +0 -0
  137. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_session_path_role_gate.py +0 -0
  138. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_smoke_chat.py +0 -0
  139. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_smoke_one_shot.py +0 -0
  140. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_smoke_phase_5_5.py +0 -0
  141. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_spawn_command.py +0 -0
  142. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_spawn_live_pin.py +0 -0
  143. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_spawn_mitosis_lineage.py +0 -0
  144. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_spawn_tmux_session.py +0 -0
  145. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_spawn_windows_relaunch.py +0 -0
  146. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_version_consistency.py +0 -0
  147. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_watchdog.py +0 -0
  148. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_watchdog_dm_wake.py +0 -0
  149. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_watchdog_dm_wake_cooldown.py +0 -0
  150. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_watchdog_dm_wake_wiring.py +0 -0
  151. {swarph_cli-0.22.0 → swarph_cli-0.23.0}/tests/test_watchdog_model_rung.py +0 -0
  152. {swarph_cli-0.22.0 → swarph_cli-0.23.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.22.0
3
+ Version: 0.23.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.22.0"
7
+ version = "0.23.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.22.0"
19
+ __version__ = "0.23.0"
20
20
 
21
21
  __all__ = ["__version__"]
@@ -0,0 +1,38 @@
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
+ try:
25
+ body_s, sig_s = token.split(".", 1)
26
+ payload = _unb64(body_s)
27
+ expected = hmac.new(secret.encode(), payload, hashlib.sha256).digest()
28
+ if not hmac.compare_digest(_unb64(sig_s), expected):
29
+ return None
30
+ d = json.loads(payload)
31
+ if not isinstance(d, dict) or {"chain_id", "depth", "jti"} - d.keys():
32
+ return None
33
+ return {"chain_id": d["chain_id"], "depth": int(d["depth"]), "jti": d["jti"]}
34
+ except (ValueError, TypeError, KeyError, json.JSONDecodeError):
35
+ # the known malformed-input paths (split / b64 / json / int / dict). Fail
36
+ # safe to None. NOT a bare `Exception`: an unexpected error should surface
37
+ # as a bug, not silently masquerade as "invalid token".
38
+ 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.22.0
3
+ Version: 0.23.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,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,27 @@
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
@@ -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