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
node/transport/rung.py ADDED
@@ -0,0 +1,276 @@
1
+ """One rung of the transport ladder, and the four the node has.
2
+
3
+ The router used to know how every rung dials: four if-blocks, each with
4
+ its own seal, its own breaker lookup, its own cascade note. Now a rung is
5
+ one object that says whether it is up, whether it can reach a peer and
6
+ where, and makes one delivery attempt; the router walks the ladder and
7
+ keeps the books. A rung that is a compartment elsewhere (the relay rung,
8
+ the libp2p transport) is reached through a thin adapter here so that it
9
+ need not know the ladder exists.
10
+
11
+ Adapters read their daemon off the router at call time (`router.fips`,
12
+ `router.iroh`, `router.p2p`) rather than at construction: a test swaps a
13
+ fake in after the router is built, and a sidecar registers itself later.
14
+
15
+ Every rung answers the same four questions the same way - up?, where?,
16
+ deliver, and what it did (health(), fault_log()) - so the router walks the
17
+ ladder blindly for all of them. There is no rung the router knows by name.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ from typing import Any, Callable, Mapping, Optional, Protocol, TypedDict, runtime_checkable
23
+
24
+ from . import fips_rung as fips_transport
25
+ from .const import TransportError
26
+ from .ladder import FaultLog
27
+
28
+ Verdict = dict[str, tuple[bool, str]]
29
+ Jwe = Mapping[str, Any] # a DIDComm JWE as didcomm.encrypt returns it
30
+ Seal = Callable[[Mapping[str, Any], str, bool], Jwe]
31
+
32
+
33
+ class RungHealth(TypedDict, total=False):
34
+ """What a rung reports about itself; the daemon-backed rungs add their own keys."""
35
+ available: bool
36
+
37
+
38
+ @runtime_checkable
39
+ class RungProtocol(Protocol):
40
+ """What the ladder needs from a rung, and nothing it does not."""
41
+
42
+ name: str # the scheme negotiate() ranks: 'fips-native', 'iroh', 'libp2p', 'nostr'
43
+ rank: int # this node's preference; lower is tried first
44
+
45
+ def available(self) -> bool:
46
+ """The local side of this rung is up (daemon reachable, listener bound)."""
47
+ ...
48
+
49
+ def target_of(self, desc: Optional[Mapping[str, Any]], theirs: Mapping[str, int]) -> Optional[str]:
50
+ """Where this rung would reach the peer that published `desc`, or
51
+ None when the descriptor does not advertise it. `theirs` is the
52
+ peer's scheme -> preference map already read from the descriptor."""
53
+ ...
54
+
55
+ def deliver(self, message: Mapping[str, Any], to_pubkey: str, target: str, anonymous: bool) -> Verdict:
56
+ """One attempt. Returns {scheme: (delivered, note)}; raises on failure
57
+ so the ladder records it and moves on."""
58
+ ...
59
+
60
+ def health(self) -> Optional[Mapping[str, Any]]:
61
+ """What this rung reports about itself, or None when its daemon is
62
+ not built; the router keys it by rung name."""
63
+ ...
64
+
65
+ def fault_log(self) -> Optional[FaultLog]:
66
+ """The log of what this rung refused or lost, or None when it keeps
67
+ none; the router merges every log it is given."""
68
+ ...
69
+
70
+
71
+ class Rung:
72
+ """Defaults for a rung that has nothing to report. The four rungs here
73
+ and any test double inherit it so the ladder never probes for a method."""
74
+
75
+ name: str = 'rung'
76
+ rank: int = 50
77
+
78
+ def available(self) -> bool:
79
+ return False
80
+
81
+ def target_of(self, desc: Optional[Mapping[str, Any]], theirs: Mapping[str, int]) -> Optional[str]:
82
+ return None
83
+
84
+ def deliver(self, message: Mapping[str, Any], to_pubkey: str, target: str, anonymous: bool) -> Verdict:
85
+ raise TransportError('%s rung does not deliver' % self.name)
86
+
87
+ def health(self) -> Optional[Mapping[str, Any]]:
88
+ return None
89
+
90
+ def fault_log(self) -> Optional[FaultLog]:
91
+ return None
92
+
93
+
94
+ def _log_of(owner: Any) -> Optional[FaultLog]:
95
+ log = getattr(owner, 'faults', None)
96
+ return log if isinstance(log, FaultLog) else None
97
+
98
+
99
+ def endpoints_of(desc: Optional[Mapping[str, Any]]) -> tuple[dict[str, int], dict[str, str]]:
100
+ """A descriptor's advertised rungs: (scheme -> preference, scheme -> address).
101
+
102
+ The 'key' tag with a fips transport key counts as a fips-native endpoint
103
+ at the strongest preference, the way the handshake reads it.
104
+ """
105
+ theirs: dict[str, int] = {}
106
+ where: dict[str, str] = {}
107
+ if desc is None:
108
+ return theirs, where
109
+ for t in desc.get('tags', []):
110
+ if not t:
111
+ continue
112
+ if t[0] == 'endpoint' and len(t) > 2:
113
+ scheme = 'fips-native' if t[1] == 'fips' else t[1]
114
+ pref = int(t[3]) if len(t) > 3 and str(t[3]).isdigit() else 50
115
+ theirs[scheme] = pref
116
+ where[scheme] = t[2]
117
+ elif t[0] == 'key' and len(t) > 2 and t[1] == 'fips':
118
+ where['fips-native'] = t[2]
119
+ theirs['fips-native'] = 5
120
+ return theirs, where
121
+
122
+
123
+ class FipsRung(Rung):
124
+ """Datagram flows through the fips sidecar; mesh-encrypted by the daemon."""
125
+
126
+ name = 'fips-native'
127
+ rank = 5
128
+
129
+ def __init__(self, router: Any, seal: Seal) -> None:
130
+ self.router = router
131
+ self.seal = seal
132
+
133
+ def available(self) -> bool:
134
+ return self.router.fips is not None
135
+
136
+ def target_of(self, desc: Optional[Mapping[str, Any]], theirs: Mapping[str, int]) -> Optional[str]:
137
+ _, where = endpoints_of(desc)
138
+ if 'fips-native' in where:
139
+ return where['fips-native']
140
+ return '' if 'fips-native' in theirs else None
141
+
142
+ def deliver(self, message: Mapping[str, Any], to_pubkey: str, target: str, anonymous: bool) -> Verdict:
143
+ fips = self.router.fips
144
+ peer_key = target or to_pubkey
145
+ port = self.router.fips_port
146
+ if ':' in peer_key and not peer_key.startswith('did:'):
147
+ host, _, p = peer_key.partition(':')
148
+ if p.isdigit():
149
+ peer_key, port = host, int(p)
150
+ their_agree = self.router.peers.agreement(to_pubkey)
151
+ if their_agree is None:
152
+ raise TransportError('no live agreement key for peer %s' % to_pubkey[:16])
153
+ flow = fips.open_flow(peer_key, port=port)
154
+ jwe = self.seal(message, to_pubkey, anonymous)
155
+ raw = json.dumps(jwe, separators=(',', ':'), sort_keys=True).encode('utf-8')
156
+ # Fragments to the flow's max_payload; raises rather than silently
157
+ # dropping an oversize datagram, so the ladder cascades.
158
+ fips.send_message(flow, raw, flags=fips_transport.FLAG_DIDCOMM)
159
+ return {self.name: (True, 'delivered via fips native flow to %s:%d' % (peer_key, port))}
160
+
161
+ def health(self) -> Optional[Mapping[str, Any]]:
162
+ fips = self.router.fips
163
+ if fips is None:
164
+ return None
165
+ return {
166
+ 'available': fips.is_available(),
167
+ 'sock_path': getattr(fips, 'sock_path', None),
168
+ 'port': self.router.fips_port,
169
+ 'listening': getattr(self.router, '_fips_listener_sock', None) is not None,
170
+ 'flows_count': len(getattr(fips, 'open_flows', {})),
171
+ 'listeners_count': len(getattr(fips, 'listeners', {})),
172
+ 'success_count': self.router.success_counts['fips-native'],
173
+ }
174
+
175
+
176
+ class IrohRung(Rung):
177
+ """EXPERIMENTAL. A direct stream the wire itself neither encrypts nor
178
+ authenticates; only the JWE inside does. Built only when configured."""
179
+
180
+ name = 'iroh'
181
+ rank = 10
182
+
183
+ def __init__(self, router: Any) -> None:
184
+ self.router = router
185
+
186
+ def available(self) -> bool:
187
+ return self.router.iroh is not None
188
+
189
+ def target_of(self, desc: Optional[Mapping[str, Any]], theirs: Mapping[str, int]) -> Optional[str]:
190
+ _, where = endpoints_of(desc)
191
+ # Without a ticket there is nothing to dial, whatever the preference says.
192
+ return where.get('iroh') or None
193
+
194
+ def deliver(self, message: Mapping[str, Any], to_pubkey: str, target: str, anonymous: bool) -> Verdict:
195
+ res: Verdict = self.router.iroh.send(message, to_pubkey, ticket=target, anonymous=anonymous)
196
+ return res
197
+
198
+ def health(self) -> Optional[Mapping[str, Any]]:
199
+ iroh = self.router.iroh
200
+ return None if iroh is None else dict(iroh.health)
201
+
202
+ def fault_log(self) -> Optional[FaultLog]:
203
+ return _log_of(self.router.iroh)
204
+
205
+
206
+ class Libp2pRung(Rung):
207
+ """A stream through the p2p sidecar; the target is derived from the
208
+ descriptor's transport key, so the dial is bound to what it signed."""
209
+
210
+ name = 'libp2p'
211
+ rank = 20
212
+
213
+ def __init__(self, router: Any, seal: Seal) -> None:
214
+ self.router = router
215
+ self.seal = seal
216
+
217
+ def available(self) -> bool:
218
+ p2p = self.router.p2p
219
+ return p2p is not None and bool(getattr(p2p, 'connected', False))
220
+
221
+ def target_of(self, desc: Optional[Mapping[str, Any]], theirs: Mapping[str, int]) -> Optional[str]:
222
+ if 'libp2p' not in theirs:
223
+ return None
224
+ from ..planes.transport.p2p import compartment as p2p_wire
225
+ target, why = p2p_wire.target_of(desc)
226
+ # An advertised but unusable endpoint is still an attempt the ladder
227
+ # should record as this rung's failure, not skip in silence.
228
+ return target if target is not None else 'unusable: %s' % why
229
+
230
+ def deliver(self, message: Mapping[str, Any], to_pubkey: str, target: str, anonymous: bool) -> Verdict:
231
+ if target.startswith('unusable: '):
232
+ raise TransportError('libp2p endpoint ' + target)
233
+ their_agree = self.router.peers.agreement(to_pubkey)
234
+ if their_agree is None:
235
+ raise TransportError('no live agreement key for peer %s' % to_pubkey[:16])
236
+ jwe = self.seal(message, to_pubkey, anonymous)
237
+ sent, note = self.router.p2p.send_direct(target, jwe)
238
+ if not sent:
239
+ raise TransportError(note or 'libp2p stream not acknowledged')
240
+ return {self.name: (True, note)}
241
+
242
+ def health(self) -> Optional[Mapping[str, Any]]:
243
+ p2p = self.router.p2p
244
+ return None if p2p is None else RungHealth(available=self.available())
245
+
246
+
247
+ class RelayRungAdapter(Rung):
248
+ """The floor of the ladder: gift-wraps over the relays. Reaches every
249
+ peer, so it never says no; the relay rung itself reports which relays
250
+ took the wrap."""
251
+
252
+ name = 'nostr'
253
+ rank = 100
254
+
255
+ def __init__(self, router: Any) -> None:
256
+ self.router = router
257
+
258
+ def available(self) -> bool:
259
+ return True
260
+
261
+ def target_of(self, desc: Optional[Mapping[str, Any]], theirs: Mapping[str, int]) -> Optional[str]:
262
+ return ''
263
+
264
+ def deliver(self, message: Mapping[str, Any], to_pubkey: str, target: str, anonymous: bool) -> Verdict:
265
+ res: Verdict = self.router.nostr.send(message, to_pubkey, anonymous=anonymous, stored=False)
266
+ return res
267
+
268
+ def health(self) -> Optional[Mapping[str, Any]]:
269
+ return dict(self.router.nostr.health)
270
+
271
+ def fault_log(self) -> Optional[FaultLog]:
272
+ return _log_of(self.router.nostr)
273
+
274
+
275
+ __all__ = ['FipsRung', 'IrohRung', 'Jwe', 'Libp2pRung', 'RelayRungAdapter', 'RungHealth', 'RungProtocol', 'Seal',
276
+ 'Verdict', 'endpoints_of']
@@ -0,0 +1,166 @@
1
+ """Secure Scuttlebutt (SSB) transport ladder rung for TransportRouter.
2
+
3
+ Integrates SSB peer-to-peer delivery into the Fleetmesh transport ladder:
4
+ - Checks sidecar availability over local Unix domain socket.
5
+ - Resolves peer SSB endpoints from node descriptors (scheme 'ssb').
6
+ - Delivers messages to remote SSB peers via SHS BoxStream.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from typing import Any, Callable, Mapping, Optional
12
+
13
+ from node.transport.const import TransportError
14
+ from node.transport.ladder import FaultLog
15
+ from node.transport.rung import Rung, Verdict, endpoints_of
16
+ from sidecar.fleetmesh_ssb import SSBSidecarClient
17
+
18
+ log = logging.getLogger('ssb_rung')
19
+
20
+ __all__ = [
21
+ 'SSBRung',
22
+ ]
23
+
24
+
25
+ class SSBRung(Rung):
26
+ """Transport rung delegating outbound deliveries to the SSB sidecar daemon."""
27
+
28
+ name: str = 'ssb'
29
+ rank: int = 25 # P2P tier between libp2p and relay
30
+
31
+ def __init__(
32
+ self,
33
+ router: Optional[Any] = None,
34
+ sidecar_client: Optional[SSBSidecarClient] = None,
35
+ socket_path: Optional[str] = None,
36
+ clock: Optional[Callable[[], int]] = None,
37
+ ):
38
+ self.router = router
39
+ self._client = sidecar_client
40
+ self._socket_path = socket_path
41
+ clk = clock or (getattr(router, 'clock', None) if router else None)
42
+ self._faults = FaultLog('ssb', clock=clk)
43
+
44
+ @property
45
+ def faults(self) -> FaultLog:
46
+ return self._faults
47
+
48
+ def fault_log(self) -> Optional[FaultLog]:
49
+ return self._faults
50
+
51
+ def _get_client(self) -> Optional[SSBSidecarClient]:
52
+ if self._client is not None:
53
+ return self._client
54
+ sock_path = self._socket_path
55
+ if sock_path is None and self.router is not None:
56
+ node_dir = getattr(self.router, 'dir', None) or getattr(self.router, 'node_dir', None)
57
+ if node_dir:
58
+ import os
59
+ sock_path = os.path.join(node_dir, 'ssb.sock')
60
+ if sock_path:
61
+ self._client = SSBSidecarClient(sock_path)
62
+ return self._client
63
+
64
+ def available(self) -> bool:
65
+ """Returns True if the SSB sidecar socket is reachable and healthy."""
66
+ client = self._get_client()
67
+ if client is None:
68
+ return False
69
+ try:
70
+ st = client.status()
71
+ return bool(st.get('ok'))
72
+ except Exception:
73
+ return False
74
+
75
+ def target_of(self, desc: Optional[Mapping[str, Any]], theirs: Mapping[str, int]) -> Optional[str]:
76
+ """Extract SSB endpoint target from descriptor."""
77
+ if desc is None:
78
+ return None
79
+ _, where = endpoints_of(desc)
80
+ if 'ssb' in where:
81
+ return where['ssb']
82
+
83
+ # Check raw tags
84
+ for t in desc.get('tags', []):
85
+ if len(t) > 2 and t[0] in ('endpoint', 'address') and t[1] == 'ssb':
86
+ return t[2]
87
+ if len(t) > 2 and t[0] == 'ssb':
88
+ return t[1]
89
+ return None
90
+
91
+ def deliver(
92
+ self,
93
+ message: Mapping[str, Any],
94
+ to_pubkey: str,
95
+ target: str,
96
+ anonymous: bool = False,
97
+ ) -> Verdict:
98
+ """Attempt delivery of message to target SSB endpoint."""
99
+ client = self._get_client()
100
+ if client is None:
101
+ self._faults.record('ssb-client-unavailable', ident=to_pubkey)
102
+ raise TransportError('SSB sidecar client not configured')
103
+
104
+ # Parse target format (e.g. "net:192.168.1.100:8008~shs:@pubkey.ed25519" or "host:port:feed_id")
105
+ host = "127.0.0.1"
106
+ port = 8008
107
+ feed_id = target
108
+
109
+ if '~shs:' in target:
110
+ addr_part, shs_part = target.split('~shs:', 1)
111
+ feed_id = shs_part
112
+ if addr_part.startswith('net:'):
113
+ addr_part = addr_part[4:]
114
+ parts = addr_part.split(':')
115
+ if len(parts) >= 2:
116
+ host = parts[0]
117
+ port = int(parts[1])
118
+ elif ':' in target:
119
+ parts = target.split(':')
120
+ if len(parts) == 3:
121
+ host, port_str, feed_id = parts
122
+ port = int(port_str)
123
+
124
+ try:
125
+ # 1. Connect / dial peer if not connected
126
+ dial_res = client.dial(host=host, port=port, feed_id=feed_id)
127
+ if not dial_res.get('ok') and dial_res.get('error') != 'already connected':
128
+ err_msg = dial_res.get('error', 'unknown dial failure')
129
+ self._faults.record(f'dial-failed: {err_msg}', ident=to_pubkey)
130
+ raise TransportError(f"SSB dial failed: {err_msg}")
131
+
132
+ # 2. Publish message payload
133
+ post_res = client.post({
134
+ "type": "fleetmesh-transport",
135
+ "recipient": to_pubkey,
136
+ "payload": dict(message),
137
+ "anonymous": anonymous,
138
+ })
139
+
140
+ if not post_res.get('ok'):
141
+ err_msg = post_res.get('error', 'unknown post failure')
142
+ self._faults.record(f'post-failed: {err_msg}', ident=to_pubkey)
143
+ raise TransportError(f"SSB post failed: {err_msg}")
144
+
145
+ return {self.name: (True, 'delivered via ssb sidecar')}
146
+
147
+ except Exception as e:
148
+ if not isinstance(e, TransportError):
149
+ self._faults.record(str(e), ident=to_pubkey)
150
+ raise TransportError(f"SSB delivery error: {e}") from e
151
+ raise
152
+
153
+ def health(self) -> Optional[Mapping[str, Any]]:
154
+ client = self._get_client()
155
+ if client is None:
156
+ return {"available": False, "status": "no_client"}
157
+ try:
158
+ st = client.status()
159
+ return {
160
+ "available": bool(st.get("ok")),
161
+ "feed_id": st.get("feed_id"),
162
+ "peers_connected": st.get("peers_connected", 0),
163
+ "messages_count": st.get("messages_count", 0),
164
+ }
165
+ except Exception as e:
166
+ return {"available": False, "error": str(e)}