fleetmesh-node 0.2.0__py3-none-any.whl

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 (338) hide show
  1. fleetmesh_node-0.2.0.dist-info/METADATA +57 -0
  2. fleetmesh_node-0.2.0.dist-info/RECORD +338 -0
  3. fleetmesh_node-0.2.0.dist-info/WHEEL +5 -0
  4. fleetmesh_node-0.2.0.dist-info/entry_points.txt +2 -0
  5. fleetmesh_node-0.2.0.dist-info/licenses/LICENSE +121 -0
  6. fleetmesh_node-0.2.0.dist-info/top_level.txt +2 -0
  7. node/RELEASE +1 -0
  8. node/__init__.py +35 -0
  9. node/app/__init__.py +49 -0
  10. node/app/commands.py +265 -0
  11. node/app/config.py +131 -0
  12. node/app/control.py +670 -0
  13. node/app/core.py +867 -0
  14. node/app/plugins.py +76 -0
  15. node/app/router.py +83 -0
  16. node/app/socket.py +431 -0
  17. node/app/systemd.py +36 -0
  18. node/app/views.py +204 -0
  19. node/channels/__init__.py +378 -0
  20. node/channels/bus.py +331 -0
  21. node/channels/journal.py +46 -0
  22. node/commands/__init__.py +140 -0
  23. node/commands/adapter.py +210 -0
  24. node/commands/base.py +278 -0
  25. node/commands/cluster_cmds.py +183 -0
  26. node/commands/compute_cmds.py +401 -0
  27. node/commands/core.py +474 -0
  28. node/commands/gossip_cmds.py +106 -0
  29. node/commands/nsite_cmds.py +141 -0
  30. node/commands/peering.py +402 -0
  31. node/commands/plugin_cmds.py +135 -0
  32. node/commands/sidecar_cmds.py +120 -0
  33. node/commands/sync.py +388 -0
  34. node/commands/transport_cmds.py +451 -0
  35. node/commands/video_cmds.py +99 -0
  36. node/compartment/__init__.py +124 -0
  37. node/compartment/base.py +868 -0
  38. node/compartment/command.py +458 -0
  39. node/compartment/context.py +163 -0
  40. node/compartment/every.py +108 -0
  41. node/compartment/handler.py +131 -0
  42. node/compartment/lifecycle.py +460 -0
  43. node/compartment/lint.py +388 -0
  44. node/compartment/need.py +158 -0
  45. node/compartment/problem.py +521 -0
  46. node/compartment/refusals.py +120 -0
  47. node/compartment/registry.py +790 -0
  48. node/compartment/schema.py +290 -0
  49. node/compartment/sidecar.py +499 -0
  50. node/compartment/stores.py +662 -0
  51. node/compartment/wire.py +225 -0
  52. node/compartment/worker.py +61 -0
  53. node/config/__init__.py +43 -0
  54. node/config/base.py +116 -0
  55. node/config/change.py +441 -0
  56. node/config/changestore.py +201 -0
  57. node/config/compartments.py +1144 -0
  58. node/config/node.py +318 -0
  59. node/config/parameter.py +563 -0
  60. node/config/protocols.py +274 -0
  61. node/config/records.py +171 -0
  62. node/examples/__init__.py +1 -0
  63. node/examples/demo.py +226 -0
  64. node/examples/follower.py +46 -0
  65. node/interfaces.py +235 -0
  66. node/keystore/__init__.py +27 -0
  67. node/keystore/core.py +103 -0
  68. node/keystore/enclave.py +688 -0
  69. node/meshnode.py +111 -0
  70. node/metrics/__init__.py +396 -0
  71. node/metrics/adapters.py +102 -0
  72. node/metrics/face.py +110 -0
  73. node/planes/__init__.py +21 -0
  74. node/planes/identity/__init__.py +20 -0
  75. node/planes/identity/announce/__init__.py +9 -0
  76. node/planes/identity/announce/compartment.py +447 -0
  77. node/planes/identity/announce/types.py +26 -0
  78. node/planes/identity/discovery/__init__.py +9 -0
  79. node/planes/identity/discovery/compartment.py +522 -0
  80. node/planes/identity/discovery/types.py +13 -0
  81. node/planes/identity/linked/__init__.py +1 -0
  82. node/planes/identity/linked/compartment.py +270 -0
  83. node/planes/identity/linked/types.py +25 -0
  84. node/planes/identity/presence/__init__.py +7 -0
  85. node/planes/identity/presence/compartment.py +409 -0
  86. node/planes/identity/presence/types.py +14 -0
  87. node/planes/operations/__init__.py +13 -0
  88. node/planes/operations/scheduler/__init__.py +9 -0
  89. node/planes/operations/scheduler/compartment.py +353 -0
  90. node/planes/operations/scheduler/types.py +21 -0
  91. node/planes/operations/updates/__init__.py +17 -0
  92. node/planes/operations/updates/core.py +310 -0
  93. node/planes/operations/updates/store.py +79 -0
  94. node/planes/operations/updates/types.py +20 -0
  95. node/planes/peering/__init__.py +28 -0
  96. node/planes/peering/budget/__init__.py +43 -0
  97. node/planes/peering/budget/chain.py +43 -0
  98. node/planes/peering/budget/core.py +537 -0
  99. node/planes/peering/budget/evaluation.py +80 -0
  100. node/planes/peering/budget/meter.py +100 -0
  101. node/planes/peering/budget/receipts.py +439 -0
  102. node/planes/peering/budget/shape.py +101 -0
  103. node/planes/peering/budget/tick.py +110 -0
  104. node/planes/peering/budget/types.py +67 -0
  105. node/planes/peering/budget/windows.py +346 -0
  106. node/planes/peering/capacity/__init__.py +7 -0
  107. node/planes/peering/capacity/compartment.py +342 -0
  108. node/planes/peering/capacity/types.py +24 -0
  109. node/planes/peering/peering/__init__.py +25 -0
  110. node/planes/peering/peering/core.py +464 -0
  111. node/planes/peering/peering/envelope.py +142 -0
  112. node/planes/peering/peering/handshake.py +198 -0
  113. node/planes/peering/peering/shape.py +80 -0
  114. node/planes/peering/peering/types.py +39 -0
  115. node/planes/peering/reporter/__init__.py +42 -0
  116. node/planes/peering/reporter/constants.py +29 -0
  117. node/planes/peering/reporter/core.py +299 -0
  118. node/planes/peering/reporter/finding.py +74 -0
  119. node/planes/peering/reporter/findings.py +171 -0
  120. node/planes/peering/reporter/publish.py +285 -0
  121. node/planes/peering/reporter/reading.py +193 -0
  122. node/planes/peering/reporter/shape.py +86 -0
  123. node/planes/peering/reporter/types.py +50 -0
  124. node/planes/peering/strain/__init__.py +9 -0
  125. node/planes/peering/strain/compartment.py +204 -0
  126. node/planes/peering/strain/types.py +21 -0
  127. node/planes/peering/trust/__init__.py +9 -0
  128. node/planes/peering/trust/compartment.py +239 -0
  129. node/planes/peering/trust/types.py +30 -0
  130. node/planes/services/__init__.py +33 -0
  131. node/planes/services/compute/__init__.py +160 -0
  132. node/planes/services/compute/common.py +134 -0
  133. node/planes/services/compute/compartment.py +681 -0
  134. node/planes/services/compute/executors/__init__.py +121 -0
  135. node/planes/services/compute/executors/agent.py +840 -0
  136. node/planes/services/compute/executors/bacalhau.py +605 -0
  137. node/planes/services/compute/executors/base.py +127 -0
  138. node/planes/services/compute/executors/harness.py +439 -0
  139. node/planes/services/compute/executors/mcp.py +454 -0
  140. node/planes/services/compute/executors/media.py +898 -0
  141. node/planes/services/compute/executors/nondet.py +283 -0
  142. node/planes/services/compute/executors/providers.py +499 -0
  143. node/planes/services/compute/executors/registry.py +140 -0
  144. node/planes/services/compute/executors/wasm.py +244 -0
  145. node/planes/services/compute/executors/zk.py +604 -0
  146. node/planes/services/compute/jobs.py +227 -0
  147. node/planes/services/compute/load.py +180 -0
  148. node/planes/services/compute/market.py +433 -0
  149. node/planes/services/compute/sandbox.py +282 -0
  150. node/planes/services/compute/types.py +84 -0
  151. node/planes/services/compute_wire/__init__.py +58 -0
  152. node/planes/services/compute_wire/compartment.py +256 -0
  153. node/planes/services/compute_wire/messages.py +320 -0
  154. node/planes/services/compute_wire/sealing.py +147 -0
  155. node/planes/services/compute_wire/session.py +572 -0
  156. node/planes/services/compute_wire/types.py +49 -0
  157. node/planes/services/compute_wire/verify.py +331 -0
  158. node/planes/services/dvm/__init__.py +24 -0
  159. node/planes/services/dvm/gateway.py +526 -0
  160. node/planes/services/dvm/protocol.py +35 -0
  161. node/planes/services/dvm/translate.py +15 -0
  162. node/planes/services/dvm/types.py +25 -0
  163. node/planes/services/gateway/__init__.py +40 -0
  164. node/planes/services/gateway/settle.py +279 -0
  165. node/planes/services/gateway/translate.py +644 -0
  166. node/planes/services/gossip/__init__.py +22 -0
  167. node/planes/services/gossip/core.py +929 -0
  168. node/planes/services/gossip/envelope.py +154 -0
  169. node/planes/services/gossip/mesh.py +127 -0
  170. node/planes/services/gossip/types.py +50 -0
  171. node/planes/services/nsite/__init__.py +7 -0
  172. node/planes/services/nsite/client.py +62 -0
  173. node/planes/services/nsite/compartment.py +752 -0
  174. node/planes/services/nsite/types.py +44 -0
  175. node/planes/services/paidfetch/__init__.py +7 -0
  176. node/planes/services/paidfetch/compartment.py +862 -0
  177. node/planes/services/paidfetch/types.py +99 -0
  178. node/planes/services/settle/__init__.py +27 -0
  179. node/planes/services/settle/compartment.py +384 -0
  180. node/planes/services/settle/engine.py +456 -0
  181. node/planes/services/settle/gateways.py +541 -0
  182. node/planes/services/settle/payment.py +512 -0
  183. node/planes/services/settle/store.py +74 -0
  184. node/planes/services/settle/types.py +58 -0
  185. node/planes/services/sidecars/__init__.py +8 -0
  186. node/planes/services/sidecars/compartment.py +461 -0
  187. node/planes/services/sidecars/manifest.py +304 -0
  188. node/planes/services/sidecars/manifests/10-grasp.json +37 -0
  189. node/planes/services/sidecars/manifests/20-ncontainer.json +31 -0
  190. node/planes/services/sidecars/manifests/30-magpie.json +28 -0
  191. node/planes/services/sidecars/types.py +43 -0
  192. node/planes/services/video/__init__.py +106 -0
  193. node/planes/services/video/barter.py +407 -0
  194. node/planes/services/video/cli.py +158 -0
  195. node/planes/services/video/compartment.py +932 -0
  196. node/planes/services/video/ingest.py +524 -0
  197. node/planes/services/video/ledger.py +705 -0
  198. node/planes/services/video/packager.py +847 -0
  199. node/planes/services/video/player.py +456 -0
  200. node/planes/services/video/timed_segments.py +124 -0
  201. node/planes/services/video/transit.py +478 -0
  202. node/planes/store/__init__.py +24 -0
  203. node/planes/store/blob_replication/__init__.py +7 -0
  204. node/planes/store/blob_replication/compartment.py +496 -0
  205. node/planes/store/blob_replication/types.py +35 -0
  206. node/planes/store/blob_wire/__init__.py +48 -0
  207. node/planes/store/blob_wire/client.py +57 -0
  208. node/planes/store/blob_wire/core.py +359 -0
  209. node/planes/store/blob_wire/intake.py +74 -0
  210. node/planes/store/blob_wire/messages.py +85 -0
  211. node/planes/store/projected/__init__.py +7 -0
  212. node/planes/store/projected/compartment.py +258 -0
  213. node/planes/store/projected/types.py +29 -0
  214. node/planes/store/retention/__init__.py +7 -0
  215. node/planes/store/retention/compartment.py +214 -0
  216. node/planes/store/retention/types.py +12 -0
  217. node/planes/store/sync/__init__.py +35 -0
  218. node/planes/store/sync/core.py +311 -0
  219. node/planes/store/sync/envelope.py +106 -0
  220. node/planes/store/sync/handover.py +206 -0
  221. node/planes/store/sync/initiator.py +128 -0
  222. node/planes/store/sync/responder.py +114 -0
  223. node/planes/store/sync/rules.py +179 -0
  224. node/planes/store/sync/shape.py +80 -0
  225. node/planes/store/sync/types.py +51 -0
  226. node/planes/transport/__init__.py +23 -0
  227. node/planes/transport/bridge/__init__.py +13 -0
  228. node/planes/transport/bridge/compartment.py +785 -0
  229. node/planes/transport/bridge/ratelimit.py +44 -0
  230. node/planes/transport/bridge/types.py +43 -0
  231. node/planes/transport/lan/__init__.py +9 -0
  232. node/planes/transport/lan/compartment.py +446 -0
  233. node/planes/transport/lan/types.py +25 -0
  234. node/planes/transport/link/__init__.py +7 -0
  235. node/planes/transport/link/compartment.py +257 -0
  236. node/planes/transport/link/types.py +27 -0
  237. node/planes/transport/mediation/__init__.py +45 -0
  238. node/planes/transport/mediation/compartment.py +238 -0
  239. node/planes/transport/mediation/leaf.py +107 -0
  240. node/planes/transport/mediation/messages.py +302 -0
  241. node/planes/transport/mediation/service.py +233 -0
  242. node/planes/transport/mediation/store.py +37 -0
  243. node/planes/transport/mediation/types.py +20 -0
  244. node/planes/transport/p2p/__init__.py +7 -0
  245. node/planes/transport/p2p/compartment.py +349 -0
  246. node/planes/transport/p2p/types.py +22 -0
  247. node/protocols/__init__.py +277 -0
  248. node/protocols/activitypub/__init__.py +56 -0
  249. node/protocols/activitypub/actor.py +230 -0
  250. node/protocols/activitypub/federation.py +490 -0
  251. node/protocols/activitypub/media.py +145 -0
  252. node/protocols/activitypub/models.py +286 -0
  253. node/protocols/activitypub/package.py +271 -0
  254. node/protocols/activitypub/store.py +564 -0
  255. node/protocols/arcade/__init__.py +26 -0
  256. node/protocols/atproto/__init__.py +13 -0
  257. node/protocols/atproto/firehose.py +126 -0
  258. node/protocols/atproto/metering.py +165 -0
  259. node/protocols/atproto/package.py +155 -0
  260. node/protocols/atproto/repo.py +250 -0
  261. node/protocols/atproto/xrpc.py +285 -0
  262. node/protocols/base.py +109 -0
  263. node/protocols/circles/__init__.py +29 -0
  264. node/protocols/dm/__init__.py +50 -0
  265. node/protocols/grasp/__init__.py +22 -0
  266. node/protocols/isotime.py +15 -0
  267. node/protocols/nostr/__init__.py +51 -0
  268. node/protocols/nostr/blobstore.py +383 -0
  269. node/protocols/nostr/blossom.py +627 -0
  270. node/protocols/nostr/client.py +377 -0
  271. node/protocols/nostr/descriptor.py +211 -0
  272. node/protocols/nostr/eventstore.py +304 -0
  273. node/protocols/nostr/package.py +182 -0
  274. node/protocols/nostr/peers.py +109 -0
  275. node/protocols/nostr/relayserver.py +656 -0
  276. node/protocols/p2panda/__init__.py +22 -0
  277. node/protocols/p2panda/identity.py +19 -0
  278. node/protocols/p2panda/package.py +236 -0
  279. node/protocols/p2panda/rung.py +152 -0
  280. node/protocols/p2panda/store.py +273 -0
  281. node/protocols/rhio/__init__.py +33 -0
  282. node/protocols/rhio/package.py +275 -0
  283. node/protocols/rhio/rung.py +136 -0
  284. node/protocols/rhio/store.py +1423 -0
  285. node/protocols/ssb/muxrpc.py +256 -0
  286. node/protocols/ssb/package.py +267 -0
  287. node/protocols/textile/__init__.py +111 -0
  288. node/protocols/textile/transducers.py +259 -0
  289. node/release.py +119 -0
  290. node/storage/__init__.py +88 -0
  291. node/storage/accounts.py +305 -0
  292. node/storage/adapter.py +161 -0
  293. node/storage/base.py +385 -0
  294. node/storage/dialect.py +209 -0
  295. node/storage/grants.py +317 -0
  296. node/storage/locator.py +150 -0
  297. node/storage/postgres.py +294 -0
  298. node/storage/projected.py +953 -0
  299. node/storage/sharding.py +448 -0
  300. node/storage/standing.py +568 -0
  301. node/testnet/__init__.py +9 -0
  302. node/testnet/cluster.py +731 -0
  303. node/testnet/fips_daemon.py +230 -0
  304. node/transport/__init__.py +94 -0
  305. node/transport/const.py +110 -0
  306. node/transport/didcomm_inbound.py +251 -0
  307. node/transport/dnstxt.py +169 -0
  308. node/transport/fips_rung/__init__.py +67 -0
  309. node/transport/fips_rung/core.py +666 -0
  310. node/transport/fips_rung/listener.py +96 -0
  311. node/transport/fips_rung/types.py +12 -0
  312. node/transport/host.py +334 -0
  313. node/transport/iroh_rung/__init__.py +29 -0
  314. node/transport/iroh_rung/core.py +477 -0
  315. node/transport/iroh_rung/quic.py +942 -0
  316. node/transport/iroh_rung/types.py +22 -0
  317. node/transport/ladder.py +145 -0
  318. node/transport/lineproto.py +49 -0
  319. node/transport/mdns.py +303 -0
  320. node/transport/netfetch.py +283 -0
  321. node/transport/relay_rung/__init__.py +23 -0
  322. node/transport/relay_rung/cooldown.py +87 -0
  323. node/transport/relay_rung/core.py +138 -0
  324. node/transport/relay_rung/inbound.py +161 -0
  325. node/transport/relay_rung/ledger.py +104 -0
  326. node/transport/relay_rung/listen.py +121 -0
  327. node/transport/relay_rung/outbound.py +181 -0
  328. node/transport/relay_rung/shape.py +71 -0
  329. node/transport/relay_rung/types.py +58 -0
  330. node/transport/relay_rung/wire.py +63 -0
  331. node/transport/router.py +761 -0
  332. node/transport/rung.py +276 -0
  333. node/transport/ssb_rung.py +166 -0
  334. node/transport/ws.py +502 -0
  335. sidecar/fleetmesh_announce.py +338 -0
  336. sidecar/fleetmesh_p2p.py +373 -0
  337. sidecar/fleetmesh_rhio.py +352 -0
  338. sidecar/fleetmesh_ssb.py +603 -0
@@ -0,0 +1,603 @@
1
+ #!/usr/bin/env python3
2
+ """Secure Scuttlebutt (SSB) Sidecar Daemon.
3
+
4
+ fleetmesh-ssb DIR [--port N] [--socket PATH] [--network-key HEX]
5
+
6
+ Runs the Secure Scuttlebutt peer-to-peer transport daemon in a decoupled process:
7
+ - Listens on TCP (default port 8008) for inbound SSB peers.
8
+ - Performs 4-phase Secret Handshake (SHS) mutual authentication.
9
+ - Wraps connections in 34-byte Box Stream duplex encryption.
10
+ - Multiplexes MuxRPC procedure calls (createHistoryStream, whoami, blobs).
11
+ - Communicates with meshnode over local Unix domain socket (ssb.sock) using
12
+ newline-delimited JSON IPC protocol `fleetmesh-ssb/1`.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import base64
18
+ import hashlib
19
+ import json
20
+ import logging
21
+ import os
22
+ import signal
23
+ import socket
24
+ import sys
25
+ import threading
26
+ import time
27
+ from typing import Any, Mapping, MutableMapping, Optional, Sequence
28
+
29
+ _ROOT = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
30
+ _CORE = os.path.abspath(os.path.join(_ROOT, '..', 'core'))
31
+ if _ROOT not in sys.path:
32
+ sys.path.insert(0, _ROOT)
33
+ if _CORE not in sys.path:
34
+ sys.path.insert(0, _CORE)
35
+
36
+ from cryptography.hazmat.primitives.asymmetric import ed25519
37
+
38
+ from fleetmesh_core.crypto.ssb import (
39
+ BOX_STREAM_HEADER_LENGTH,
40
+ SSB_MAINNET_NETWORK_KEY,
41
+ BoxStreamDecryptor,
42
+ BoxStreamEncryptor,
43
+ SHSClient,
44
+ SHSServer,
45
+ format_ssb_feed_id,
46
+ parse_ssb_feed_id,
47
+ )
48
+ from node.protocols.ssb.muxrpc import (
49
+ MuxRPCMessage,
50
+ MuxRPCRouter,
51
+ build_response,
52
+ build_stream_end,
53
+ )
54
+
55
+ log = logging.getLogger('fleetmesh-ssb')
56
+
57
+ PROTOCOL_VERSION = "fleetmesh-ssb/1"
58
+ DEFAULT_PORT = 8008
59
+
60
+
61
+ class SSBSidecarClient:
62
+ """IPC client used by meshnode compartments to communicate with the SSB daemon."""
63
+
64
+ def __init__(self, socket_path: str, timeout: float = 10.0):
65
+ self.socket_path = socket_path
66
+ self.timeout = timeout
67
+ self._sock: Optional[socket.socket] = None
68
+ self._lock = threading.RLock()
69
+
70
+ def connect(self) -> None:
71
+ with self._lock:
72
+ if self._sock is not None:
73
+ return
74
+ s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
75
+ s.settimeout(self.timeout)
76
+ s.connect(self.socket_path)
77
+ self._sock = s
78
+
79
+ def close(self) -> None:
80
+ with self._lock:
81
+ if self._sock:
82
+ try:
83
+ self._sock.close()
84
+ except Exception:
85
+ pass
86
+ self._sock = None
87
+
88
+ def send_command(self, cmd: Mapping[str, Any]) -> Mapping[str, Any]:
89
+ """Send a command dictionary and receive response."""
90
+ with self._lock:
91
+ if self._sock is None:
92
+ self.connect()
93
+
94
+ assert self._sock is not None
95
+ payload = json.dumps(cmd).encode('utf-8') + b'\n'
96
+ self._sock.sendall(payload)
97
+
98
+ # Read response line
99
+ buf = bytearray()
100
+ while b'\n' not in buf:
101
+ chunk = self._sock.recv(4096)
102
+ if not chunk:
103
+ raise ConnectionResetError("SSB daemon closed IPC connection")
104
+ buf.extend(chunk)
105
+
106
+ line = bytes(buf.split(b'\n', 1)[0])
107
+ return json.loads(line.decode('utf-8'))
108
+
109
+ def status(self) -> Mapping[str, Any]:
110
+ return self.send_command({"cmd": "status"})
111
+
112
+ def post(self, content: Mapping[str, Any]) -> Mapping[str, Any]:
113
+ return self.send_command({"cmd": "post", "content": content})
114
+
115
+ def dial(self, host: str, port: int, feed_id: str) -> Mapping[str, Any]:
116
+ return self.send_command({"cmd": "dial", "host": host, "port": port, "feed_id": feed_id})
117
+
118
+
119
+ class SSBSidecarDaemon:
120
+ """The Secure Scuttlebutt TCP listener, Box Stream router, and IPC daemon."""
121
+
122
+ def __init__(
123
+ self,
124
+ node_dir: str,
125
+ tcp_port: int = DEFAULT_PORT,
126
+ bind_host: str = "0.0.0.0",
127
+ uds_path: Optional[str] = None,
128
+ network_key: bytes = SSB_MAINNET_NETWORK_KEY,
129
+ secret_key_bytes: Optional[bytes] = None,
130
+ ):
131
+ self.node_dir = node_dir
132
+ self.tcp_port = tcp_port
133
+ self.bind_host = bind_host
134
+ self.uds_path = uds_path or os.path.join(node_dir, "ssb.sock")
135
+ self.network_key = network_key
136
+
137
+ # Key setup
138
+ if secret_key_bytes:
139
+ self.priv_key = ed25519.Ed25519PrivateKey.from_private_bytes(secret_key_bytes)
140
+ else:
141
+ self.priv_key = self._load_or_create_key()
142
+
143
+ self.sk_bytes = self.priv_key.private_bytes_raw()
144
+ self.pk_bytes = self.priv_key.public_key().public_bytes_raw()
145
+ self.feed_id = format_ssb_feed_id(self.pk_bytes)
146
+
147
+ # MuxRPC Router
148
+ self.router = MuxRPCRouter(local_feed_id=self.feed_id)
149
+ self._register_rpc_methods()
150
+
151
+ # State
152
+ self._running = False
153
+ self._tcp_sock: Optional[socket.socket] = None
154
+ self._uds_sock: Optional[socket.socket] = None
155
+ self._threads: list[threading.Thread] = []
156
+ self._ipc_clients: list[socket.socket] = []
157
+ self._ipc_subscribers: set[socket.socket] = set()
158
+ self._peers: MutableMapping[str, Any] = {}
159
+ self._lock = threading.RLock()
160
+
161
+ # Local feed store (in-memory for demo / extensible to sqlite)
162
+ self._messages: list[Mapping[str, Any]] = []
163
+
164
+ def _load_or_create_key(self) -> ed25519.Ed25519PrivateKey:
165
+ """Load ed25519 key from keys.json or generate new one."""
166
+ keys_file = os.path.join(self.node_dir, "keys.json")
167
+ if os.path.exists(keys_file):
168
+ try:
169
+ with open(keys_file, "r", encoding="utf-8") as f:
170
+ data = json.load(f)
171
+ seed_hex = data.get("transport_seed") or data.get("ed25519_seed")
172
+ if seed_hex:
173
+ seed = bytes.fromhex(seed_hex)[:32]
174
+ return ed25519.Ed25519PrivateKey.from_private_bytes(seed)
175
+ except Exception as e:
176
+ log.warning("failed to load key from %s: %s", keys_file, e)
177
+
178
+ return ed25519.Ed25519PrivateKey.generate()
179
+
180
+ def _register_rpc_methods(self) -> None:
181
+ """Register Scuttlebutt standard MuxRPC procedures."""
182
+ def _handle_create_history_stream(req_id: int, args: list[Any], rpc_type: str) -> list[MuxRPCMessage]:
183
+ target_feed = None
184
+ if args and isinstance(args[0], dict):
185
+ target_feed = args[0].get("id")
186
+
187
+ with self._lock:
188
+ matching = [
189
+ m for m in self._messages
190
+ if not target_feed or m.get("author") == target_feed
191
+ ]
192
+
193
+ out = []
194
+ for msg_doc in matching:
195
+ out.append(build_response(req_id, msg_doc, is_stream=True))
196
+ out.append(build_stream_end(req_id))
197
+ return out
198
+
199
+ self.router.register("createHistoryStream", _handle_create_history_stream)
200
+
201
+ def start(self) -> None:
202
+ """Start TCP listener and IPC Unix domain socket listener."""
203
+ self._running = True
204
+
205
+ # 1. Bind IPC UDS socket
206
+ if os.path.exists(self.uds_path):
207
+ try:
208
+ os.unlink(self.uds_path)
209
+ except Exception:
210
+ pass
211
+
212
+ os.makedirs(os.path.dirname(os.path.abspath(self.uds_path)), exist_ok=True)
213
+ self._uds_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
214
+ self._uds_sock.bind(self.uds_path)
215
+ self._uds_sock.listen(16)
216
+ self._uds_sock.settimeout(1.0)
217
+
218
+ # 2. Bind TCP server socket
219
+ self._tcp_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
220
+ self._tcp_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
221
+ self._tcp_sock.bind((self.bind_host, self.tcp_port))
222
+ # Update tcp_port if port 0 was passed
223
+ self.tcp_port = self._tcp_sock.getsockname()[1]
224
+ self._tcp_sock.listen(32)
225
+ self._tcp_sock.settimeout(1.0)
226
+
227
+ # Launch accept threads
228
+ t_ipc = threading.Thread(target=self._ipc_accept_loop, daemon=True, name="ssb-ipc-accept")
229
+ t_tcp = threading.Thread(target=self._tcp_accept_loop, daemon=True, name="ssb-tcp-accept")
230
+ t_ipc.start()
231
+ t_tcp.start()
232
+ self._threads.extend([t_ipc, t_tcp])
233
+ log.info("SSB sidecar started. Feed: %s, TCP: %s:%d, IPC: %s", self.feed_id, self.bind_host, self.tcp_port, self.uds_path)
234
+
235
+ def stop(self) -> None:
236
+ """Stop daemon and close sockets."""
237
+ self._running = False
238
+ with self._lock:
239
+ if self._tcp_sock:
240
+ try:
241
+ self._tcp_sock.close()
242
+ except Exception:
243
+ pass
244
+ if self._uds_sock:
245
+ try:
246
+ self._uds_sock.close()
247
+ except Exception:
248
+ pass
249
+ if os.path.exists(self.uds_path):
250
+ try:
251
+ os.unlink(self.uds_path)
252
+ except Exception:
253
+ pass
254
+
255
+ for t in self._threads:
256
+ t.join(timeout=1.0)
257
+
258
+ # -------------------------------------------------------------------------
259
+ # IPC Loop (Unix Domain Socket)
260
+ # -------------------------------------------------------------------------
261
+
262
+ def _ipc_accept_loop(self) -> None:
263
+ while self._running:
264
+ try:
265
+ assert self._uds_sock is not None
266
+ conn, _ = self._uds_sock.accept()
267
+ t = threading.Thread(target=self._handle_ipc_client, args=(conn,), daemon=True)
268
+ t.start()
269
+ except (socket.timeout, OSError):
270
+ continue
271
+
272
+ def _handle_ipc_client(self, conn: socket.socket) -> None:
273
+ with self._lock:
274
+ self._ipc_clients.append(conn)
275
+
276
+ buf = bytearray()
277
+ try:
278
+ while self._running:
279
+ chunk = conn.recv(4096)
280
+ if not chunk:
281
+ break
282
+ buf.extend(chunk)
283
+ while b'\n' in buf:
284
+ line, buf = buf.split(b'\n', 1)
285
+ if not line.strip():
286
+ continue
287
+ try:
288
+ cmd = json.loads(line.decode('utf-8'))
289
+ resp = self._dispatch_ipc_command(conn, cmd)
290
+ except Exception as e:
291
+ resp = {"ok": False, "error": str(e)}
292
+
293
+ conn.sendall(json.dumps(resp).encode('utf-8') + b'\n')
294
+ except Exception:
295
+ pass
296
+ finally:
297
+ with self._lock:
298
+ if conn in self._ipc_clients:
299
+ self._ipc_clients.remove(conn)
300
+ self._ipc_subscribers.discard(conn)
301
+ try:
302
+ conn.close()
303
+ except Exception:
304
+ pass
305
+
306
+ def _dispatch_ipc_command(self, conn: socket.socket, cmd: Mapping[str, Any]) -> Mapping[str, Any]:
307
+ action = cmd.get("cmd")
308
+ if action == "status":
309
+ return {
310
+ "ok": True,
311
+ "protocol": PROTOCOL_VERSION,
312
+ "feed_id": self.feed_id,
313
+ "port": self.tcp_port,
314
+ "peers_connected": len(self._peers),
315
+ "messages_count": len(self._messages),
316
+ }
317
+
318
+ elif action == "subscribe":
319
+ with self._lock:
320
+ self._ipc_subscribers.add(conn)
321
+ return {"ok": True, "status": "subscribed"}
322
+
323
+ elif action == "post":
324
+ content = cmd.get("content", {})
325
+ msg = self.publish_post(content)
326
+ return {"ok": True, "message": msg}
327
+
328
+ elif action == "dial":
329
+ host = cmd.get("host", "127.0.0.1")
330
+ port = int(cmd.get("port", DEFAULT_PORT))
331
+ remote_feed_id = cmd.get("feed_id", "")
332
+ success, err = self.dial_peer(host, port, remote_feed_id)
333
+ if success:
334
+ return {"ok": True, "status": "connected"}
335
+ return {"ok": False, "error": err}
336
+
337
+ return {"ok": False, "error": f"unknown command '{action}'"}
338
+
339
+ # -------------------------------------------------------------------------
340
+ # TCP Accept Loop & Peer Connections
341
+ # -------------------------------------------------------------------------
342
+
343
+ def _tcp_accept_loop(self) -> None:
344
+ while self._running:
345
+ try:
346
+ assert self._tcp_sock is not None
347
+ conn, addr = self._tcp_sock.accept()
348
+ t = threading.Thread(target=self._handle_inbound_peer, args=(conn, addr), daemon=True)
349
+ t.start()
350
+ except (socket.timeout, OSError):
351
+ continue
352
+
353
+ def _handle_inbound_peer(self, sock: socket.socket, addr: tuple[str, int]) -> None:
354
+ """Handle incoming TCP connection from a remote SSB peer."""
355
+ sock.settimeout(15.0)
356
+ try:
357
+ # 1. SHS Handshake (as Server)
358
+ server = SHSServer(self.sk_bytes, network_key=self.network_key)
359
+
360
+ # Phase 1: Client Hello
361
+ msg1 = self._recv_exact(sock, 64)
362
+ server.verify_client_hello(msg1)
363
+
364
+ # Phase 2: Server Hello
365
+ msg2 = server.create_server_hello()
366
+ sock.sendall(msg2)
367
+
368
+ # Phase 3: Client Authenticate
369
+ msg3 = self._recv_exact(sock, 112)
370
+ remote_pk = server.verify_client_authenticate(msg3)
371
+ remote_feed = format_ssb_feed_id(remote_pk)
372
+
373
+ # Phase 4: Server Accept
374
+ msg4 = server.create_server_accept()
375
+ sock.sendall(msg4)
376
+
377
+ # Box Stream session keys
378
+ enc_k, enc_n, dec_k, dec_n = server.get_box_stream_keys()
379
+ enc = BoxStreamEncryptor(enc_k, enc_n)
380
+ dec = BoxStreamDecryptor(dec_k, dec_n)
381
+
382
+ with self._lock:
383
+ self._peers[remote_feed] = {"addr": addr, "sock": sock}
384
+
385
+ log.info("Inbound SSB peer handshake authenticated: %s from %s", remote_feed, addr)
386
+
387
+ # Multiplex MuxRPC over BoxStream
388
+ self._muxrpc_loop(sock, enc, dec, remote_feed)
389
+
390
+ except Exception as e:
391
+ log.debug("SSB inbound peer session terminated: %s", e)
392
+ finally:
393
+ try:
394
+ sock.close()
395
+ except Exception:
396
+ pass
397
+
398
+ def dial_peer(self, host: str, port: int, remote_feed_id: str) -> tuple[bool, str]:
399
+ """Dial a remote peer, execute SHS, and start MuxRPC loop."""
400
+ try:
401
+ remote_pk = parse_ssb_feed_id(remote_feed_id)
402
+ except Exception as e:
403
+ return False, f"invalid feed ID: {e}"
404
+
405
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
406
+ sock.settimeout(15.0)
407
+ try:
408
+ sock.connect((host, port))
409
+ client = SHSClient(self.sk_bytes, server_ed25519_pk=remote_pk, network_key=self.network_key)
410
+
411
+ # Phase 1: Client Hello
412
+ msg1 = client.create_client_hello()
413
+ sock.sendall(msg1)
414
+
415
+ # Phase 2: Server Hello
416
+ msg2 = self._recv_exact(sock, 64)
417
+ client.verify_server_hello(msg2)
418
+
419
+ # Phase 3: Client Authenticate
420
+ msg3 = client.create_client_authenticate()
421
+ sock.sendall(msg3)
422
+
423
+ # Phase 4: Server Accept
424
+ msg4 = self._recv_exact(sock, 80)
425
+ client.verify_server_accept(msg4)
426
+
427
+ # Box Stream session keys
428
+ enc_k, enc_n, dec_k, dec_n = client.get_box_stream_keys()
429
+ enc = BoxStreamEncryptor(enc_k, enc_n)
430
+ dec = BoxStreamDecryptor(dec_k, dec_n)
431
+
432
+ with self._lock:
433
+ self._peers[remote_feed_id] = {"addr": (host, port), "sock": sock}
434
+
435
+ t = threading.Thread(
436
+ target=self._muxrpc_loop,
437
+ args=(sock, enc, dec, remote_feed_id),
438
+ daemon=True,
439
+ )
440
+ t.start()
441
+ return True, ""
442
+ except Exception as e:
443
+ try:
444
+ sock.close()
445
+ except Exception:
446
+ pass
447
+ return False, str(e)
448
+
449
+ def _muxrpc_loop(
450
+ self,
451
+ sock: socket.socket,
452
+ enc: BoxStreamEncryptor,
453
+ dec: BoxStreamDecryptor,
454
+ remote_feed_id: str,
455
+ ) -> None:
456
+ """Read and write MuxRPC packets over decrypted Box Stream."""
457
+ muxrpc_buf = bytearray()
458
+ try:
459
+ while self._running:
460
+ # Read 34-byte Box Stream header
461
+ hdr_bytes = self._recv_exact(sock, BOX_STREAM_HEADER_LENGTH)
462
+ length, tag = dec.decrypt_header(hdr_bytes)
463
+ if length is None or tag is None:
464
+ # Stream goodbye received
465
+ break
466
+
467
+ body_ciphertext = self._recv_exact(sock, length)
468
+ payload_plaintext = dec.decrypt_body(tag, body_ciphertext)
469
+ muxrpc_buf.extend(payload_plaintext)
470
+
471
+ # Parse any complete MuxRPC messages
472
+ while muxrpc_buf:
473
+ rpc_msg, consumed = MuxRPCMessage.from_bytes(bytes(muxrpc_buf))
474
+ if rpc_msg is None:
475
+ break
476
+ muxrpc_buf = muxrpc_buf[consumed:]
477
+
478
+ # Dispatch request and send responses
479
+ if rpc_msg.req > 0:
480
+ responses = self.router.handle_request(rpc_msg)
481
+ for r in responses:
482
+ pkt = enc.encrypt_packet(r.encode())
483
+ sock.sendall(pkt)
484
+ except Exception as e:
485
+ log.debug("MuxRPC loop ended for %s: %s", remote_feed_id, e)
486
+ finally:
487
+ with self._lock:
488
+ self._peers.pop(remote_feed_id, None)
489
+ try:
490
+ sock.sendall(enc.encrypt_goodbye())
491
+ except Exception:
492
+ pass
493
+ try:
494
+ sock.close()
495
+ except Exception:
496
+ pass
497
+
498
+ def _recv_exact(self, sock: socket.socket, num_bytes: int) -> bytes:
499
+ buf = bytearray()
500
+ while len(buf) < num_bytes and self._running:
501
+ chunk = sock.recv(num_bytes - len(buf))
502
+ if not chunk:
503
+ raise ConnectionResetError("Socket closed prematurely")
504
+ buf.extend(chunk)
505
+ return bytes(buf)
506
+
507
+ def publish_post(self, content: Mapping[str, Any]) -> Mapping[str, Any]:
508
+ """Create and sign a new post message on the local feed."""
509
+ with self._lock:
510
+ seq = len(self._messages) + 1
511
+ prev = self._messages[-1].get("key") if self._messages else None
512
+ now_ms = int(time.time() * 1000)
513
+
514
+ msg_value = {
515
+ "previous": prev,
516
+ "author": self.feed_id,
517
+ "sequence": seq,
518
+ "timestamp": now_ms,
519
+ "hash": "sha256",
520
+ "content": dict(content),
521
+ }
522
+
523
+ canonical = json.dumps(msg_value, sort_keys=True, separators=(',', ':')).encode('utf-8')
524
+ sig = self.priv_key.sign(canonical)
525
+ sig_b64 = base64.b64encode(sig).decode('ascii') + ".sig.ed25519"
526
+ msg_value["signature"] = sig_b64
527
+
528
+ msg_hash = hashlib.sha256(canonical).digest()
529
+ msg_key = f"%{base64.b64encode(msg_hash).decode('ascii')}.sha256"
530
+
531
+ record = {
532
+ "key": msg_key,
533
+ "value": msg_value,
534
+ "timestamp": now_ms,
535
+ }
536
+ self._messages.append(record)
537
+
538
+ # Notify IPC listeners
539
+ self._notify_ipc({
540
+ "event": "message",
541
+ "feed_id": self.feed_id,
542
+ "sequence": seq,
543
+ "message": record,
544
+ })
545
+
546
+ return record
547
+
548
+ def _notify_ipc(self, event_doc: Mapping[str, Any]) -> None:
549
+ payload = json.dumps(event_doc).encode('utf-8') + b'\n'
550
+ with self._lock:
551
+ dead = []
552
+ for client in self._ipc_subscribers:
553
+ try:
554
+ client.sendall(payload)
555
+ except Exception:
556
+ dead.append(client)
557
+ for d in dead:
558
+ self._ipc_subscribers.discard(d)
559
+
560
+
561
+ def main(argv: Optional[Sequence[str]] = None) -> int:
562
+ parser = argparse.ArgumentParser(description="Fleetmesh Secure Scuttlebutt Sidecar Daemon")
563
+ parser.add_argument("dir", help="Node state directory containing keys.json")
564
+ parser.add_argument("--port", type=int, default=DEFAULT_PORT, help=f"TCP listen port (default {DEFAULT_PORT})")
565
+ parser.add_argument("--host", default="0.0.0.0", help="TCP bind address (default 0.0.0.0)")
566
+ parser.add_argument("--socket", help="Path to Unix domain socket")
567
+ parser.add_argument("--network-key", help="Hex encoded 32-byte network capability key")
568
+
569
+ args = parser.parse_args(argv)
570
+
571
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
572
+
573
+ net_key = bytes.fromhex(args.network_key) if args.network_key else SSB_MAINNET_NETWORK_KEY
574
+
575
+ daemon = SSBSidecarDaemon(
576
+ node_dir=args.dir,
577
+ tcp_port=args.port,
578
+ bind_host=args.host,
579
+ uds_path=args.socket,
580
+ network_key=net_key,
581
+ )
582
+
583
+ def _sig_handler(sig, frame):
584
+ log.info("Received termination signal %s, shutting down...", sig)
585
+ daemon.stop()
586
+ sys.exit(0)
587
+
588
+ signal.signal(signal.SIGINT, _sig_handler)
589
+ signal.signal(signal.SIGTERM, _sig_handler)
590
+
591
+ daemon.start()
592
+
593
+ try:
594
+ while daemon._running:
595
+ time.sleep(1.0)
596
+ except KeyboardInterrupt:
597
+ daemon.stop()
598
+
599
+ return 0
600
+
601
+
602
+ if __name__ == '__main__':
603
+ sys.exit(main())