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,256 @@
1
+ """Secure Scuttlebutt (SSB) MuxRPC Protocol Framing and Method Dispatcher (SSB-MuxRPC).
2
+
3
+ Standard library only. Manages:
4
+ - 9-byte packet framing (flags, body length, request ID) for SSB-MuxRPC.
5
+ - Supported payload encodings: binary (00), UTF-8 text (01), JSON (10).
6
+ - Procedure calls: async, sync, source, duplex, sink.
7
+ - Feed history streaming (createHistoryStream) with stream termination.
8
+ - Error propagation and stream completion flags.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import struct
14
+ from typing import Any, Callable, Mapping, Optional, Sequence, Union
15
+
16
+ from fleetmesh_core.conformance.errors import FleetmeshError
17
+
18
+ __all__ = [
19
+ 'MuxRPCError',
20
+ 'PS_TYPE_BUFFER',
21
+ 'PS_TYPE_TEXT',
22
+ 'PS_TYPE_JSON',
23
+ 'FLAG_STREAM',
24
+ 'FLAG_END_ERR',
25
+ 'MuxRPCMessage',
26
+ 'build_request',
27
+ 'build_response',
28
+ 'build_error_response',
29
+ 'build_stream_end',
30
+ 'MuxRPCRouter',
31
+ ]
32
+
33
+ PS_TYPE_BUFFER = 0
34
+ PS_TYPE_TEXT = 1
35
+ PS_TYPE_JSON = 2
36
+
37
+ FLAG_STREAM = 0x08
38
+ FLAG_END_ERR = 0x04
39
+
40
+
41
+ class MuxRPCError(FleetmeshError):
42
+ """Base error for MuxRPC operations."""
43
+
44
+
45
+ MAX_MUXRPC_BODY_LEN = 16 * 1024 * 1024 # 16 MB
46
+
47
+
48
+ class MuxRPCMessage:
49
+ """A single MuxRPC packet with a 9-byte binary header and body."""
50
+
51
+ def __init__(
52
+ self,
53
+ req: int,
54
+ body: Union[bytes, str, Mapping[str, Any], list[Any], bool, int, float, None],
55
+ body_type: int = PS_TYPE_JSON,
56
+ is_stream: bool = False,
57
+ is_end_err: bool = False,
58
+ ):
59
+ self.req = req
60
+ self.body = body
61
+ self.body_type = body_type
62
+ self.is_stream = is_stream
63
+ self.is_end_err = is_end_err
64
+
65
+ @property
66
+ def flags(self) -> int:
67
+ f = self.body_type & 0x03
68
+ if self.is_stream:
69
+ f |= FLAG_STREAM
70
+ if self.is_end_err:
71
+ f |= FLAG_END_ERR
72
+ return f
73
+
74
+ @property
75
+ def raw_body_bytes(self) -> bytes:
76
+ if self.body_type == PS_TYPE_BUFFER:
77
+ return bytes(self.body) if isinstance(self.body, (bytes, bytearray)) else b""
78
+ elif self.body_type == PS_TYPE_TEXT:
79
+ return str(self.body).encode('utf-8')
80
+ elif self.body_type == PS_TYPE_JSON:
81
+ return json.dumps(self.body, separators=(',', ':')).encode('utf-8')
82
+ return b""
83
+
84
+ def encode(self) -> bytes:
85
+ """Encode to 9-byte header followed by body bytes."""
86
+ body_bytes = self.raw_body_bytes
87
+ header = struct.pack('>BIi', self.flags, len(body_bytes), self.req)
88
+ return header + body_bytes
89
+
90
+ @classmethod
91
+ def from_bytes(cls, data: bytes) -> tuple[Optional[MuxRPCMessage], int]:
92
+ """Decode message from wire bytes.
93
+
94
+ Returns (message, bytes_consumed) if a full message is available.
95
+ Returns (None, 0) if more bytes are needed.
96
+ """
97
+ if len(data) < 9:
98
+ return None, 0
99
+
100
+ flags, length, req = struct.unpack('>BIi', data[:9])
101
+ if length > MAX_MUXRPC_BODY_LEN:
102
+ raise MuxRPCError(f"MuxRPC packet length exceeds maximum ({length} > {MAX_MUXRPC_BODY_LEN})")
103
+
104
+ total_len = 9 + length
105
+ if len(data) < total_len:
106
+ return None, 0
107
+
108
+ body_raw = data[9:total_len]
109
+ body_type = flags & 0x03
110
+ is_stream = bool(flags & FLAG_STREAM)
111
+ is_end_err = bool(flags & FLAG_END_ERR)
112
+
113
+ body: Any
114
+ if body_type == PS_TYPE_BUFFER:
115
+ body = body_raw
116
+ elif body_type == PS_TYPE_TEXT:
117
+ body = body_raw.decode('utf-8', errors='replace')
118
+ elif body_type == PS_TYPE_JSON:
119
+ if not body_raw:
120
+ body = None
121
+ else:
122
+ try:
123
+ body = json.loads(body_raw.decode('utf-8'))
124
+ except Exception as e:
125
+ raise MuxRPCError(f"invalid JSON payload in MuxRPC packet: {e}") from e
126
+ else:
127
+ body = body_raw
128
+
129
+ msg = cls(
130
+ req=req,
131
+ body=body,
132
+ body_type=body_type,
133
+ is_stream=is_stream,
134
+ is_end_err=is_end_err,
135
+ )
136
+ return msg, total_len
137
+
138
+ def __repr__(self) -> str:
139
+ return (
140
+ f"<MuxRPCMessage req={self.req} type={self.body_type} "
141
+ f"stream={self.is_stream} end_err={self.is_end_err} body={self.body!r}>"
142
+ )
143
+
144
+
145
+ def build_request(
146
+ req_id: int,
147
+ name: Sequence[str],
148
+ args: Sequence[Any],
149
+ rpc_type: str = "async",
150
+ ) -> MuxRPCMessage:
151
+ """Build an outbound MuxRPC request message."""
152
+ body = {
153
+ "name": list(name),
154
+ "args": list(args),
155
+ "type": rpc_type,
156
+ }
157
+ is_stream = rpc_type in ("source", "duplex", "sink")
158
+ return MuxRPCMessage(
159
+ req=req_id,
160
+ body=body,
161
+ body_type=PS_TYPE_JSON,
162
+ is_stream=is_stream,
163
+ is_end_err=False,
164
+ )
165
+
166
+
167
+ def build_response(
168
+ req_id: int,
169
+ payload: Any,
170
+ is_stream: bool = False,
171
+ is_end_err: bool = False,
172
+ body_type: int = PS_TYPE_JSON,
173
+ ) -> MuxRPCMessage:
174
+ """Build a standard response message (response req is negative)."""
175
+ return MuxRPCMessage(
176
+ req=-abs(req_id),
177
+ body=payload,
178
+ body_type=body_type,
179
+ is_stream=is_stream,
180
+ is_end_err=is_end_err,
181
+ )
182
+
183
+
184
+ def build_error_response(req_id: int, message: str, is_stream: bool = False) -> MuxRPCMessage:
185
+ """Build an error response packet with end_err=True."""
186
+ return MuxRPCMessage(
187
+ req=-abs(req_id),
188
+ body={"name": "Error", "message": message},
189
+ body_type=PS_TYPE_JSON,
190
+ is_stream=is_stream,
191
+ is_end_err=True,
192
+ )
193
+
194
+
195
+ def build_stream_end(req_id: int) -> MuxRPCMessage:
196
+ """Build an end-of-stream indicator (JSON true with stream and end_err set)."""
197
+ return MuxRPCMessage(
198
+ req=-abs(req_id),
199
+ body=True,
200
+ body_type=PS_TYPE_JSON,
201
+ is_stream=True,
202
+ is_end_err=True,
203
+ )
204
+
205
+
206
+ class MuxRPCRouter:
207
+ """Dispatches MuxRPC procedure calls to registered handlers."""
208
+
209
+ def __init__(self, local_feed_id: str = "@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=.ed25519"):
210
+ self.local_feed_id = local_feed_id
211
+ self._handlers: dict[str, Callable[[int, list[Any], str], list[MuxRPCMessage]]] = {}
212
+ self._register_default_handlers()
213
+
214
+ def register(
215
+ self,
216
+ name: str,
217
+ handler: Callable[[int, list[Any], str], list[MuxRPCMessage]],
218
+ ) -> None:
219
+ """Register a procedure handler."""
220
+ self._handlers[name] = handler
221
+
222
+ def _register_default_handlers(self) -> None:
223
+ # whoami
224
+ def _handle_whoami(req_id: int, args: list[Any], rpc_type: str) -> list[MuxRPCMessage]:
225
+ return [build_response(req_id, {"id": self.local_feed_id})]
226
+
227
+ self.register("whoami", _handle_whoami)
228
+
229
+ # blobs.has
230
+ def _handle_blobs_has(req_id: int, args: list[Any], rpc_type: str) -> list[MuxRPCMessage]:
231
+ # Expects list of blob hashes, returns list of booleans
232
+ return [build_response(req_id, [False] * len(args))]
233
+
234
+ self.register("blobs.has", _handle_blobs_has)
235
+
236
+ def handle_request(self, msg: MuxRPCMessage) -> list[MuxRPCMessage]:
237
+ """Dispatch an incoming MuxRPC request and return zero or more response messages."""
238
+ if not isinstance(msg.body, dict):
239
+ return [build_error_response(msg.req, "Malformed RPC request: expected JSON object")]
240
+
241
+ name_list = msg.body.get("name")
242
+ if not name_list or not isinstance(name_list, list):
243
+ return [build_error_response(msg.req, "Malformed RPC request: missing or invalid 'name'")]
244
+
245
+ method_name = ".".join(name_list)
246
+ args = msg.body.get("args", [])
247
+ rpc_type = msg.body.get("type", "async")
248
+
249
+ handler = self._handlers.get(method_name)
250
+ if not handler:
251
+ return [build_error_response(msg.req, f"Method '{method_name}' not implemented")]
252
+
253
+ try:
254
+ return handler(msg.req, args, rpc_type)
255
+ except Exception as e:
256
+ return [build_error_response(msg.req, str(e))]
@@ -0,0 +1,267 @@
1
+ """fleetmesh-protocol-ssb: Secure Scuttlebutt (SSB) protocol package.
2
+
3
+ Conforms to ProtocolPackage (fleetmesh_core.wire.interfaces.ProtocolPackage).
4
+ Integrates the SSB sidecar daemon, client, MuxRPC routing, and SSBRung
5
+ into the Fleetmesh protocol host and transport ladder.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from fleetmesh_core.wire import spelling # noqa: E402
10
+
11
+ import json
12
+ import os
13
+ from typing import Any, Mapping, Optional
14
+
15
+ from fleetmesh_core.crypto.ssb import (
16
+ SSB_MAINNET_NETWORK_KEY,
17
+ SSBCryptoError,
18
+ format_ssb_feed_id,
19
+ parse_ssb_feed_id,
20
+ )
21
+ from fleetmesh_core.wire.interfaces import (
22
+ ProtocolHealth,
23
+ ProtocolHostRouter,
24
+ ProtocolIdentityAdapter,
25
+ )
26
+ from node.protocols.base import Ed25519IdentityAdapter
27
+ from node.transport.ssb_rung import SSBRung
28
+ from sidecar.fleetmesh_ssb import SSBSidecarClient, SSBSidecarDaemon
29
+
30
+ __all__ = [
31
+ 'SSBIdentityAdapter',
32
+ 'SSBProtocolPackage',
33
+ ]
34
+
35
+
36
+ class SSBIdentityAdapter(Ed25519IdentityAdapter):
37
+ """Identity and assertion adapter for Secure Scuttlebutt (@pubkey.ed25519)."""
38
+
39
+ def __init__(self, private_key: Any = None, feed_id: Optional[str] = None):
40
+ super().__init__(private_key=private_key)
41
+ self._feed_id = feed_id
42
+ if self.private_key is not None and not self._feed_id and hasattr(self.private_key, 'public_key'):
43
+ pk_raw = self.private_key.public_key().public_bytes_raw()
44
+ self._feed_id = format_ssb_feed_id(pk_raw)
45
+
46
+ def resolve_id(self, uri: str) -> Optional[str]:
47
+ """Resolves SSB feed ID from raw feed ID or ssb: URI."""
48
+ clean = uri.strip()
49
+ if clean.startswith("ssb:"):
50
+ clean = clean[4:]
51
+ if clean.startswith("@") and clean.endswith(".ed25519"):
52
+ try:
53
+ parse_ssb_feed_id(clean)
54
+ return clean
55
+ except (SSBCryptoError, ValueError):
56
+ return None
57
+ return None
58
+
59
+ def verify_assertion(self, payload: bytes, signature: bytes, pubkey: str) -> bool:
60
+ try:
61
+ if pubkey.startswith("@") and pubkey.endswith(".ed25519"):
62
+ pk_bytes = parse_ssb_feed_id(pubkey)
63
+ elif spelling.is_hex(pubkey, 64, upper=True):
64
+ pk_bytes = bytes.fromhex(pubkey)
65
+ else:
66
+ pk_bytes = pubkey.encode('utf-8')
67
+ except (SSBCryptoError, ValueError):
68
+ return False
69
+ return self._verify_ed25519_bytes(payload, signature, pk_bytes)
70
+
71
+
72
+ class SSBProtocolPackage:
73
+ """The Secure Scuttlebutt (SSB) protocol suite for Fleetmesh."""
74
+
75
+ name: str = 'ssb'
76
+ version: str = '1.0.0'
77
+ standards: tuple[str, ...] = (
78
+ 'ssb',
79
+ 'SSB-SHS',
80
+ 'SSB-BoxStream',
81
+ 'SSB-MuxRPC',
82
+ 'secret-handshake',
83
+ 'boxstream',
84
+ 'muxrpc',
85
+ )
86
+
87
+ def __init__(
88
+ self,
89
+ node_dir: Optional[str] = None,
90
+ socket_path: Optional[str] = None,
91
+ tcp_host: str = "127.0.0.1",
92
+ tcp_port: int = 8008,
93
+ network_key: bytes = SSB_MAINNET_NETWORK_KEY,
94
+ secret_key_bytes: Optional[bytes] = None,
95
+ sidecar_client: Optional[SSBSidecarClient] = None,
96
+ auto_start_sidecar: bool = False,
97
+ private_key: Optional[Any] = None,
98
+ ):
99
+ self.node_dir = node_dir or "/tmp/fleetmesh-ssb"
100
+ self.socket_path = socket_path or os.path.join(self.node_dir, "ssb.sock")
101
+ self.tcp_host = tcp_host
102
+ self.tcp_port = tcp_port
103
+ self.network_key = network_key
104
+ self.secret_key_bytes = secret_key_bytes
105
+ self.private_key = private_key
106
+ self.auto_start_sidecar = auto_start_sidecar
107
+
108
+ if self.private_key is not None and not self.secret_key_bytes:
109
+ try:
110
+ self.secret_key_bytes = self.private_key.private_bytes_raw()
111
+ except Exception:
112
+ pass
113
+
114
+ self._client = sidecar_client
115
+ self._daemon: Optional[SSBSidecarDaemon] = None
116
+ self._host: Optional[ProtocolHostRouter] = None
117
+ self._enabled: bool = True
118
+ self.config: Mapping[str, Any] = {}
119
+
120
+ def configure(self, config: Mapping[str, Any]) -> None:
121
+ self.config = dict(config)
122
+ self._enabled = self.config.get('enabled', True)
123
+ if 'socket_path' in self.config:
124
+ self.socket_path = self.config['socket_path']
125
+ if 'tcp_host' in self.config:
126
+ self.tcp_host = self.config['tcp_host']
127
+ if 'tcp_port' in self.config:
128
+ self.tcp_port = self.config['tcp_port']
129
+ if 'auto_start_sidecar' in self.config:
130
+ self.auto_start_sidecar = self.config['auto_start_sidecar']
131
+ if 'network_key' in self.config:
132
+ nk = self.config['network_key']
133
+ self.network_key = bytes.fromhex(nk) if isinstance(nk, str) else nk
134
+
135
+ @property
136
+ def client(self) -> Optional[SSBSidecarClient]:
137
+ if self._client is not None:
138
+ return self._client
139
+ if os.path.exists(self.socket_path):
140
+ self._client = SSBSidecarClient(self.socket_path)
141
+ return self._client
142
+
143
+ def identity_adapter(self, keystore: Any = None) -> Optional[ProtocolIdentityAdapter]:
144
+ priv = self.private_key
145
+ if priv is None and keystore is not None:
146
+ # Check if keystore has ed25519 key
147
+ priv = getattr(keystore, 'ed25519_sk', None)
148
+ return SSBIdentityAdapter(private_key=priv)
149
+
150
+ def mount_ingress(self, host: ProtocolHostRouter) -> None:
151
+ if not self._enabled:
152
+ return
153
+ self._host = host
154
+
155
+ # Register status inspection route for HTTP multiplexer
156
+ def _handle_ssb_status(conn: Any, req: Any = None) -> Any:
157
+ st = self.health()
158
+ payload = {
159
+ "healthy": st.healthy,
160
+ "status": st.status,
161
+ "connections_active": st.connections_active,
162
+ "messages_in": st.messages_in,
163
+ "messages_out": st.messages_out,
164
+ "errors": list(st.errors),
165
+ "socket_path": self.socket_path,
166
+ "tcp_port": self.tcp_port,
167
+ }
168
+ body = json.dumps(payload, indent=2).encode('utf-8')
169
+ if conn is not None and hasattr(conn, 'sendall'):
170
+ from node.transport import ws
171
+ ws.http_response(conn, "200 OK", body, "application/json")
172
+ return True
173
+ return 200, {"Content-Type": "application/json"}, body
174
+
175
+ if hasattr(host, 'mount_route'):
176
+ host.mount_route('/ssb/status', _handle_ssb_status, methods=('GET',), owner=self.name)
177
+ elif hasattr(host, 'register_route'):
178
+ host.register_route("GET", "/ssb/status", _handle_ssb_status, owner=self.name)
179
+
180
+ def transport_rung(self, context: Any = None) -> Optional[Any]:
181
+ if not self._enabled:
182
+ return None
183
+ router = getattr(context, 'router', None) or context
184
+ return SSBRung(
185
+ router=router,
186
+ sidecar_client=self.client,
187
+ socket_path=self.socket_path,
188
+ )
189
+
190
+ def compartments(self) -> tuple[type[Any], ...]:
191
+ return ()
192
+
193
+ def start(self) -> None:
194
+ if not self._enabled:
195
+ return
196
+ if self.auto_start_sidecar and self._daemon is None:
197
+ self._daemon = SSBSidecarDaemon(
198
+ node_dir=self.node_dir,
199
+ tcp_port=self.tcp_port,
200
+ bind_host=self.tcp_host,
201
+ uds_path=self.socket_path,
202
+ network_key=self.network_key,
203
+ secret_key_bytes=self.secret_key_bytes,
204
+ )
205
+ self._daemon.start()
206
+ self.tcp_port = self._daemon.tcp_port
207
+ self._client = SSBSidecarClient(self.socket_path)
208
+
209
+ def stop(self) -> None:
210
+ if self._client is not None:
211
+ self._client.close()
212
+ self._client = None
213
+ if self._daemon is not None:
214
+ self._daemon.stop()
215
+ self._daemon = None
216
+
217
+ def health(self) -> ProtocolHealth:
218
+ if not self._enabled:
219
+ return ProtocolHealth(
220
+ healthy=True,
221
+ status="disabled",
222
+ connections_active=0,
223
+ messages_in=0,
224
+ messages_out=0,
225
+ errors=(),
226
+ )
227
+
228
+ client = self.client
229
+ if client is not None and os.path.exists(self.socket_path):
230
+ try:
231
+ st = client.status()
232
+ if st.get('ok'):
233
+ return ProtocolHealth(
234
+ healthy=True,
235
+ status="serving",
236
+ connections_active=st.get('peers_connected', 0),
237
+ messages_in=st.get('messages_count', 0),
238
+ messages_out=0,
239
+ errors=(),
240
+ )
241
+ except Exception as e:
242
+ return ProtocolHealth(
243
+ healthy=False,
244
+ status="unreachable",
245
+ connections_active=0,
246
+ messages_in=0,
247
+ messages_out=0,
248
+ errors=(str(e),),
249
+ )
250
+
251
+ return ProtocolHealth(
252
+ healthy=True,
253
+ status="configured",
254
+ connections_active=0,
255
+ messages_in=0,
256
+ messages_out=0,
257
+ errors=(),
258
+ )
259
+
260
+ def descriptor_capabilities(self) -> tuple[tuple[str, str], ...]:
261
+ if not self._enabled:
262
+ return ()
263
+ return (
264
+ ('c', 'ssb'),
265
+ ('c', 'shs'),
266
+ ('c', 'muxrpc'),
267
+ )
@@ -0,0 +1,111 @@
1
+ """FleetMesh Node Textile Protocol Package (`node.protocols.textile`)."""
2
+ from __future__ import annotations
3
+
4
+ # The stores and route handlers live in the fleetmesh-protocols package
5
+ # (fleetmesh_protocols.textile); this package re-exports them so node code and
6
+ # the protocol router keep their import paths.
7
+
8
+ from fleetmesh_protocols.textile.fiber_routes import HttpResult, JSON_CT, binary_http_response, handle_fiber_http, is_fiber_route
9
+ from fleetmesh_protocols.textile.fiber_store import (
10
+ FIBER_KINDS,
11
+ INTERACTION_KINDS,
12
+ MAX_FIBER_BYTES,
13
+ MAX_OWNER_BYTES,
14
+ MAX_UPLOAD_BODY_BYTES,
15
+ PAYLOAD_KINDS,
16
+ VISIBILITIES,
17
+ StandaloneFiberStore,
18
+ )
19
+ from fleetmesh_protocols.textile.hosted_relay import PROTOCOL_DIGEST, PROTOCOL_VERSION, HostedLeafRelay
20
+ from fleetmesh_protocols.textile.leaf_relay import (
21
+ KIND_FABRIC,
22
+ KIND_FIBER,
23
+ KIND_QUILT,
24
+ KIND_STRAND,
25
+ NEG_BUCKETS,
26
+ NEG_FINGERPRINT,
27
+ NEG_ID_LIST,
28
+ NEG_INFINITY,
29
+ NEG_LIST_THRESHOLD,
30
+ NEG_SKIP,
31
+ NEG_VERSION,
32
+ WS_GUID,
33
+ NegBound,
34
+ NegentropyServer,
35
+ build_engine_textile_inventory,
36
+ canonical_bao_hex,
37
+ decode_neg_message,
38
+ decode_varint,
39
+ det_hex,
40
+ encode_neg_message,
41
+ encode_varint,
42
+ neg_fingerprint,
43
+ serve_relay_websocket,
44
+ )
45
+ from fleetmesh_protocols.textile.quilt_routes import handle_quilt_http, is_quilt_route
46
+ from fleetmesh_protocols.textile.quilt_store import (
47
+ AUDIENCE_RANK,
48
+ AUDIENCES,
49
+ MAX_VIEW_JSON_BYTES,
50
+ StandaloneQuiltStore,
51
+ compute_fabric_root,
52
+ compute_quilt_root,
53
+ group_owner_id,
54
+ validate_quilt_view,
55
+ )
56
+ from .transducers import PROTOCOL_FAMILIES, PythonProtocolRouter, default_python_protocol_router
57
+
58
+ __all__ = [
59
+ "HttpResult",
60
+ "JSON_CT",
61
+ "binary_http_response",
62
+ "handle_fiber_http",
63
+ "is_fiber_route",
64
+ "FIBER_KINDS",
65
+ "INTERACTION_KINDS",
66
+ "MAX_FIBER_BYTES",
67
+ "MAX_OWNER_BYTES",
68
+ "MAX_UPLOAD_BODY_BYTES",
69
+ "PAYLOAD_KINDS",
70
+ "VISIBILITIES",
71
+ "StandaloneFiberStore",
72
+ "PROTOCOL_DIGEST",
73
+ "PROTOCOL_VERSION",
74
+ "HostedLeafRelay",
75
+ "KIND_FABRIC",
76
+ "KIND_FIBER",
77
+ "KIND_QUILT",
78
+ "KIND_STRAND",
79
+ "NEG_BUCKETS",
80
+ "NEG_FINGERPRINT",
81
+ "NEG_ID_LIST",
82
+ "NEG_INFINITY",
83
+ "NEG_LIST_THRESHOLD",
84
+ "NEG_SKIP",
85
+ "NEG_VERSION",
86
+ "WS_GUID",
87
+ "NegBound",
88
+ "NegentropyServer",
89
+ "build_engine_textile_inventory",
90
+ "canonical_bao_hex",
91
+ "det_hex",
92
+ "decode_neg_message",
93
+ "decode_varint",
94
+ "encode_neg_message",
95
+ "encode_varint",
96
+ "neg_fingerprint",
97
+ "serve_relay_websocket",
98
+ "handle_quilt_http",
99
+ "is_quilt_route",
100
+ "AUDIENCE_RANK",
101
+ "AUDIENCES",
102
+ "MAX_VIEW_JSON_BYTES",
103
+ "StandaloneQuiltStore",
104
+ "compute_fabric_root",
105
+ "compute_quilt_root",
106
+ "group_owner_id",
107
+ "validate_quilt_view",
108
+ "PROTOCOL_FAMILIES",
109
+ "PythonProtocolRouter",
110
+ "default_python_protocol_router",
111
+ ]