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,96 @@
1
+ """The fips-native listener rung: the socket loop that feeds the router's direct-rung rule.
2
+
3
+ Separate from the router because it is the one piece that feeds the FIPS incoming flows
4
+ into the direct-rung rule, and nothing in the send ladder needs to see it.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ import socket
10
+ import threading
11
+ import time
12
+ from typing import (
13
+ Any,
14
+ Callable,
15
+ Optional,
16
+ )
17
+
18
+ from . import core as fips_transport
19
+
20
+ log = logging.getLogger(__name__)
21
+
22
+
23
+ class FipsRungMixin:
24
+ """Mixed into TransportRouter, which owns every attribute and method read here."""
25
+
26
+ nostr: Any
27
+ fips: Optional[Any]
28
+ _fips_listener_sock: Optional[Any]
29
+ deliver_direct: Callable[..., Optional[str]]
30
+
31
+ def _dispatch_fips_payload(self, seq: int, flags: int, payload: bytes) -> None:
32
+ """One reassembled FIPS datagram payload, through the direct-rung rule."""
33
+ self.deliver_direct('fips-native', payload, f"fips-{seq}")
34
+
35
+ def _read_fips_flow(self, flow_sock: socket.socket) -> None:
36
+ reasm = fips_transport.Reassembler()
37
+ while not self.nostr._stop.is_set():
38
+ try:
39
+ if self.fips is None:
40
+ break
41
+ flow_sock.settimeout(0.5)
42
+ data = flow_sock.recv(65536)
43
+ seq, flags, msg_id, payload = fips_transport.decode_frame_ex(data)
44
+ done = reasm.feed(seq, flags, msg_id, payload)
45
+ if done is not None:
46
+ self._dispatch_fips_payload(seq, done[0], done[1])
47
+ except (socket.timeout, TimeoutError):
48
+ continue
49
+ except OSError as e:
50
+ # The flow socket is gone (daemon closed it): this flow ends;
51
+ # the listener loop keeps accepting new ones.
52
+ log.debug('fips-native: flow reader ending: %s', e)
53
+ break
54
+ except fips_transport.FipsTransportError as e:
55
+ # A frame the decoder refused is a bad datagram, not a dead
56
+ # flow: drop it and keep reading, as the reassembler drops gaps.
57
+ log.debug('fips-native: bad frame on flow dropped: %s', e)
58
+ continue
59
+ try:
60
+ flow_sock.close()
61
+ except OSError:
62
+ pass # best-effort close; the flow may already be gone
63
+
64
+ def _listen_fips(self) -> None:
65
+ sock = self._fips_listener_sock
66
+ if sock is None or self.fips is None:
67
+ return
68
+ sock.settimeout(0.5)
69
+ listener_reasm = fips_transport.Reassembler()
70
+ while not self.nostr._stop.is_set():
71
+ try:
72
+ flow_sock, arrival_info, msg = self.fips.accept_listener_event(sock)
73
+ if flow_sock is not None:
74
+ t = threading.Thread(target=self._read_fips_flow, args=(flow_sock,), daemon=True)
75
+ t.start()
76
+ continue
77
+
78
+ # Case 2: Direct frame received on listener socket (Mock/Cluster daemon)
79
+ if msg.startswith(fips_transport.FRAME_MAGIC):
80
+ seq, fl, mid, payload = fips_transport.decode_frame_ex(msg)
81
+ done = listener_reasm.feed(seq, fl, mid, payload)
82
+ if done is not None:
83
+ self._dispatch_fips_payload(seq, done[0], done[1])
84
+ except (socket.timeout, TimeoutError):
85
+ continue
86
+ except fips_transport.FipsTransportError as e:
87
+ # An undecodable direct frame: drop it, keep listening.
88
+ log.debug('fips-native: bad direct frame dropped: %s', e)
89
+ except OSError as e:
90
+ # The listener socket itself failed. During shutdown that is
91
+ # expected; otherwise log it and keep listening after a short
92
+ # pause, since the alternative is a silent dead rung.
93
+ if self.nostr._stop.is_set():
94
+ break
95
+ log.warning('fips-native: listener error, continuing: %s', e)
96
+ time.sleep(0.01)
@@ -0,0 +1,12 @@
1
+ """Types and records for the FIPS native transport rung."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Optional, TypedDict
5
+
6
+
7
+ class FipsArrivalInfo(TypedDict, total=False):
8
+ """Arrival notice payload received alongside accepted flow socket."""
9
+ peer: Optional[str]
10
+ remote_port: Optional[int]
11
+ max_payload: Optional[int]
12
+ ticket: Optional[str]
node/transport/host.py ADDED
@@ -0,0 +1,334 @@
1
+ """ProtocolHostRouter: Unified multiplexer for HTTP and WebSocket protocol suites.
2
+
3
+ Standard library and ws.py only. Multiplexes foreign and native protocol suites
4
+ (Nostr, ATProto, ActivityPub, Blossom CAS, Prometheus metrics) over a single
5
+ public edge port (e.g. 8000 or 443 with TLS).
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import collections
10
+ import dataclasses
11
+ import logging
12
+ import socket
13
+ import threading
14
+ import time
15
+ from typing import (
16
+ Any,
17
+ Callable,
18
+ Mapping,
19
+ Optional,
20
+ Sequence,
21
+ )
22
+
23
+ from fleetmesh_core.wire.interfaces import ProtocolHealth
24
+ from . import ws
25
+
26
+ log = logging.getLogger(__name__)
27
+
28
+
29
+ @dataclasses.dataclass(frozen=True)
30
+ class HttpRoute:
31
+ prefix: str
32
+ handler: Callable[[socket.socket, ws.PlainHTTP], Any]
33
+ methods: tuple[str, ...]
34
+ owner: str = ''
35
+
36
+
37
+ @dataclasses.dataclass(frozen=True)
38
+ class WebSocketRoute:
39
+ path: str
40
+ handler: Callable[[ws.WebSocket, str], Any]
41
+ owner: str = ''
42
+
43
+
44
+ class ProtocolHost:
45
+ """Unified HTTP & WebSocket multiplexer mounted on the node's public port."""
46
+
47
+ def __init__(
48
+ self,
49
+ host: str = '127.0.0.1',
50
+ port: int = 0,
51
+ name: str = 'meshnode',
52
+ clock: Optional[Callable[[], int]] = None,
53
+ bind_wait: float = 10.0,
54
+ max_body_bytes: int = 16 * 1024 * 1024,
55
+ ) -> None:
56
+ self.host: str = host
57
+ self.port: int = port
58
+ self.name: str = name
59
+ self.clock: Callable[[], int] = clock or (lambda: int(time.time()))
60
+ self.bind_wait: float = bind_wait
61
+ self.max_body_bytes: int = max_body_bytes
62
+
63
+ self._http_routes: list[HttpRoute] = []
64
+ self._ws_routes: list[WebSocketRoute] = []
65
+ self._lock: threading.RLock = threading.RLock()
66
+
67
+ self._server: Optional[ws.Server] = None
68
+ self._started: bool = False
69
+ self._active_connections: int = 0
70
+ self._requests_in: int = 0
71
+ self._requests_out: int = 0
72
+ self._errors: collections.deque[str] = collections.deque(maxlen=100)
73
+
74
+ @property
75
+ def url(self) -> str:
76
+ if self._server is not None:
77
+ return self._server.url
78
+ return 'ws://%s:%d' % (self.host, self.port)
79
+
80
+ @property
81
+ def http_url(self) -> str:
82
+ if self._server is not None:
83
+ return 'http://%s:%d' % (self._server.host, self._server.port)
84
+ return 'http://%s:%d' % (self.host, self.port)
85
+
86
+ # ---- Route Registration --------------------------------------------------
87
+
88
+ def mount_route(
89
+ self,
90
+ prefix: str,
91
+ handler: Callable[..., Any],
92
+ methods: Sequence[str] = ('GET',),
93
+ owner: str = '',
94
+ ) -> None:
95
+ """Mount an HTTP path prefix handler.
96
+
97
+ Longest prefix matches take precedence. Handler receives `(conn, req: ws.PlainHTTP)`
98
+ and returns True if handled, or False to fall through.
99
+ """
100
+ clean_prefix = '/' + prefix.strip('/') if prefix != '/' else '/'
101
+ norm_methods = tuple(m.upper() for m in methods)
102
+ with self._lock:
103
+ # Replace existing identical prefix and owner if present
104
+ self._http_routes = [
105
+ r for r in self._http_routes
106
+ if not (r.prefix == clean_prefix and r.owner == owner and r.methods == norm_methods)
107
+ ]
108
+ self._http_routes.append(HttpRoute(
109
+ prefix=clean_prefix,
110
+ handler=handler,
111
+ methods=norm_methods,
112
+ owner=owner,
113
+ ))
114
+ # Sort by longest prefix first
115
+ self._http_routes.sort(key=lambda r: len(r.prefix), reverse=True)
116
+
117
+ def mount_websocket(
118
+ self,
119
+ path: str,
120
+ handler: Callable[[ws.WebSocket, str], Any],
121
+ owner: str = '',
122
+ ) -> None:
123
+ """Mount a WebSocket handler for an exact path or prefix."""
124
+ clean_path = '/' + path.strip('/') if path != '/' else '/'
125
+ with self._lock:
126
+ self._ws_routes = [
127
+ r for r in self._ws_routes
128
+ if not (r.path == clean_path and r.owner == owner)
129
+ ]
130
+ self._ws_routes.append(WebSocketRoute(
131
+ path=clean_path,
132
+ handler=handler,
133
+ owner=owner,
134
+ ))
135
+ self._ws_routes.sort(key=lambda r: len(r.path), reverse=True)
136
+
137
+ def unmount_all(self, owner: str) -> None:
138
+ """Remove all HTTP and WebSocket routes registered by owner."""
139
+ if not owner:
140
+ return
141
+ with self._lock:
142
+ self._http_routes = [r for r in self._http_routes if r.owner != owner]
143
+ self._ws_routes = [r for r in self._ws_routes if r.owner != owner]
144
+
145
+ # Compatibility aliases conforming to ProtocolHostRouter interface
146
+ def register_route(self, method: str, prefix: str, handler: Callable[..., Any], owner: str = '') -> None:
147
+ """Alias for mount_route for compatibility."""
148
+ self.mount_route(prefix=prefix, handler=handler, methods=(method,), owner=owner)
149
+
150
+ def register_websocket(self, path: str, handler: Callable[..., Any], owner: str = '') -> None:
151
+ """Alias for mount_websocket for compatibility."""
152
+ self.mount_websocket(path=path, handler=handler, owner=owner)
153
+
154
+ def unregister_owner(self, owner: str) -> None:
155
+ """Alias for unmount_all for compatibility."""
156
+ self.unmount_all(owner=owner)
157
+
158
+ # ---- Lifecycle -----------------------------------------------------------
159
+
160
+ def start(self) -> ProtocolHost:
161
+ with self._lock:
162
+ if self._started:
163
+ return self
164
+ self._server = ws.Server(
165
+ handler=self._dispatch_websocket,
166
+ host=self.host,
167
+ port=self.port,
168
+ http_handler=self._dispatch_http,
169
+ bind_wait=self.bind_wait,
170
+ )
171
+ self.host = self._server.host
172
+ self.port = self._server.port
173
+ self._server.start()
174
+ self._started = True
175
+ log.info("ProtocolHost serving on %s (%s)", self.url, self.http_url)
176
+ return self
177
+
178
+ @property
179
+ def server(self) -> Optional[Any]:
180
+ """Underlying ws.Server instance, preserving socket address metadata even after stop."""
181
+ return self._server
182
+
183
+ def stop(self) -> None:
184
+ with self._lock:
185
+ if not self._started or self._server is None:
186
+ return
187
+ self._server.stop()
188
+ self._started = False
189
+
190
+ # ---- Multiplexing Dispatch -----------------------------------------------
191
+
192
+ def _dispatch_http(self, conn: socket.socket, req_or_path: Any, headers: Optional[dict[bytes, bytes]] = None) -> None:
193
+ self._requests_in += 1
194
+ if isinstance(req_or_path, ws.PlainHTTP) or hasattr(req_or_path, 'headers'):
195
+ req = req_or_path
196
+ else:
197
+ path = str(req_or_path)
198
+ hdrs = headers or {}
199
+ req = ws.PlainHTTP(path=path, headers=hdrs, method='GET', sock=conn)
200
+
201
+ clean_path = req.path.split('?', 1)[0]
202
+ method = req.method.upper()
203
+
204
+ # Enforce max body size from Content-Length header if present
205
+ cl_val = None
206
+ if hasattr(req, 'headers') and isinstance(req.headers, Mapping):
207
+ cl_val = req.headers.get(b'content-length') or req.headers.get('content-length') or req.headers.get('Content-Length')
208
+ if cl_val:
209
+ try:
210
+ cl_int = int(cl_val)
211
+ if cl_int > self.max_body_bytes:
212
+ ws.http_response(conn, '413 Payload Too Large',
213
+ f'{{"error":"payload_too_large","max_bytes":{self.max_body_bytes}}}'.encode('utf-8'),
214
+ 'application/json')
215
+ self._requests_out += 1
216
+ return
217
+ if cl_int < 0:
218
+ ws.http_response(conn, '400 Bad Request',
219
+ b'{"error":"invalid_content_length"}',
220
+ 'application/json')
221
+ self._requests_out += 1
222
+ return
223
+ except (ValueError, TypeError):
224
+ ws.http_response(conn, '400 Bad Request',
225
+ b'{"error":"invalid_content_length"}',
226
+ 'application/json')
227
+ self._requests_out += 1
228
+ return
229
+
230
+ with self._lock:
231
+ routes = list(self._http_routes)
232
+
233
+ method_mismatch = False
234
+ allowed_methods: set[str] = set()
235
+
236
+ for route in routes:
237
+ prefix = route.prefix
238
+ # Match if path equals prefix or starts with prefix/
239
+ matched = False
240
+ if prefix == '/':
241
+ matched = True
242
+ elif clean_path == prefix or clean_path.startswith(prefix + '/') or clean_path.startswith(prefix + '?'):
243
+ matched = True
244
+
245
+ if matched:
246
+ if '*' in route.methods or method in route.methods:
247
+ try:
248
+ handled = route.handler(conn, req)
249
+ if handled is not False:
250
+ self._requests_out += 1
251
+ return
252
+ except Exception as e:
253
+ err = f"route {route.prefix} error: {type(e).__name__}: {e}"
254
+ self._errors.append(err)
255
+ log.exception("ProtocolHost HTTP error on %s %s: %s", method, clean_path, e)
256
+ try:
257
+ ws.http_response(conn, '500 Internal Server Error',
258
+ b'{"error":"internal_server_error"}',
259
+ 'application/json')
260
+ except OSError:
261
+ pass
262
+ return
263
+ else:
264
+ method_mismatch = True
265
+ allowed_methods.update(route.methods)
266
+
267
+ # Handle CORS OPTIONS pre-flight if unhandled
268
+ if method == 'OPTIONS':
269
+ headers_map = {
270
+ 'Allow': ', '.join(sorted(allowed_methods or {'GET', 'POST', 'OPTIONS'})),
271
+ }
272
+ ws.http_response(conn, '204 No Content', b'', 'text/plain', headers=headers_map)
273
+ self._requests_out += 1
274
+ return
275
+
276
+ if method_mismatch and allowed_methods:
277
+ headers_map = {'Allow': ', '.join(sorted(allowed_methods))}
278
+ ws.http_response(conn, '405 Method Not Allowed',
279
+ b'{"error":"method_not_allowed"}',
280
+ 'application/json', headers=headers_map)
281
+ self._requests_out += 1
282
+ return
283
+
284
+ # 404 fallback
285
+ ws.http_response(conn, '404 Not Found',
286
+ f'{{"error":"not_found","path":"{clean_path}"}}'.encode('utf-8'),
287
+ 'application/json')
288
+ self._requests_out += 1
289
+
290
+ def _dispatch_websocket(self, sock: ws.WebSocket, path: str) -> None:
291
+ self._active_connections += 1
292
+ clean_path = path.split('?', 1)[0]
293
+ with self._lock:
294
+ routes = list(self._ws_routes)
295
+
296
+ handler: Optional[Callable[[ws.WebSocket, str], Any]] = None
297
+ for r in routes:
298
+ if r.path == '/' or clean_path == r.path or clean_path.startswith(r.path + '/'):
299
+ handler = r.handler
300
+ break
301
+
302
+ if handler is None:
303
+ # No matching websocket route
304
+ try:
305
+ sock.close()
306
+ except Exception:
307
+ pass
308
+ self._active_connections = max(0, self._active_connections - 1)
309
+ return
310
+
311
+ try:
312
+ handler(sock, path)
313
+ except (ws.ConnectionClosed, ConnectionResetError, BrokenPipeError, OSError):
314
+ pass
315
+ except Exception as e:
316
+ err = f"ws {clean_path} error: {type(e).__name__}: {e}"
317
+ self._errors.append(err)
318
+ log.exception("ProtocolHost WebSocket error on %s: %s", clean_path, e)
319
+ finally:
320
+ self._active_connections = max(0, self._active_connections - 1)
321
+
322
+ # ---- Health & Telemetry --------------------------------------------------
323
+
324
+ def health(self) -> ProtocolHealth:
325
+ with self._lock:
326
+ errs = list(self._errors)
327
+ return ProtocolHealth(
328
+ healthy=self._started,
329
+ status="serving" if self._started else "stopped",
330
+ connections_active=self._active_connections,
331
+ messages_in=self._requests_in,
332
+ messages_out=self._requests_out,
333
+ errors=tuple(errs[-10:]),
334
+ )
@@ -0,0 +1,29 @@
1
+ """Iroh QUIC / stream transport rung package.
2
+
3
+ Exports the IrohTransport adapter, IrohTicket parser, and QUIC live engine.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ from typing import Any
8
+
9
+ from .types import IrohHealthReport
10
+ from fleetmesh_core.identity.iroh_ticket import ALPN, IrohTicket
11
+
12
+ __all__ = [
13
+ 'ALPN',
14
+ 'DEFAULT_PREFERENCE',
15
+ 'IrohHealthReport',
16
+ 'IrohTicket',
17
+ 'IrohTransport',
18
+ 'MAX_STREAM_PAYLOAD',
19
+ 'SOCK',
20
+ 'sidecar_socket_path',
21
+ ]
22
+
23
+ def __getattr__(name: str) -> Any:
24
+ if name in ('IrohTransport', 'DEFAULT_PREFERENCE', 'MAX_STREAM_PAYLOAD', 'SOCK', 'sidecar_socket_path'):
25
+ from . import core
26
+ val = getattr(core, name)
27
+ globals()[name] = val
28
+ return val
29
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")