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,478 @@
1
+ """Transit Reflector Manager, Capacity Shed Handover & MoQ Ring Buffer (Phase 3).
2
+
3
+ Coordinates:
4
+ - Automated origin-to-supernode handover driven by `planes.peering.capacity`
5
+ shed transitions (`narrow_scope` / `amend_down` vs. `restore_below`).
6
+ - Real-time Media-over-QUIC (MoQ) / FSP datagram reflection on port 30820 with
7
+ a bounded drop-on-congestion ring buffer.
8
+ - Soft debt ceiling degradation (`degraded-audio-only`) when a pro-bono origin
9
+ exceeds `max_unsettled_transit_bytes`.
10
+ - Rhio NATS JetStream cluster fanout (`fleetmesh.video.<stream_id>`).
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import collections
15
+ import dataclasses
16
+ import hashlib
17
+ import threading
18
+ import time
19
+ from typing import Any, Callable, Deque, Mapping, Optional, Sequence, TypedDict
20
+
21
+ from fleetmesh_core.crypto import crypto
22
+ from fleetmesh_core.wire.cbor import cbor_decode, cbor_encode
23
+ from fleetmesh_core.wire.video_transit import (
24
+ COUNTER_TRANSIT_BYTES_OUT,
25
+ PORT_VIDEO_STREAM,
26
+ TransitCloseBody,
27
+ TransitGrantBody,
28
+ TransitMode,
29
+ TransitReceiptBody,
30
+ TransitSolicitationBody,
31
+ receipt_link,
32
+ )
33
+ from .ledger import TransitLedgerStore
34
+ from .packager import VideoStreamPackager
35
+
36
+
37
+ class MoQFrameDict(TypedDict, total=False):
38
+ stream_id: str
39
+ broadcaster: str
40
+ group_id: int
41
+ object_id: int
42
+ track: str
43
+ is_keyframe: bool
44
+ is_audio_only: bool
45
+ payload: bytes
46
+ sha256: str
47
+ timestamp: int
48
+ sig: str
49
+
50
+
51
+ class ReflectionResult(TypedDict):
52
+ accepted: bool
53
+ forwarded: bool
54
+ degraded_audio_only: bool
55
+ dropped_reason: str
56
+ bytes_fanned_out: int
57
+ ring_depth: int
58
+ rhio_published: bool
59
+
60
+
61
+ @dataclasses.dataclass(frozen=True)
62
+ class MoQFrame:
63
+ """Real-time interactive media frame for FIPS FSP (port 30820) and Iroh UDP streams."""
64
+ stream_id: str
65
+ broadcaster: str
66
+ group_id: int
67
+ object_id: int
68
+ track: str = 'video'
69
+ is_keyframe: bool = False
70
+ is_audio_only: bool = False
71
+ payload: bytes = b''
72
+ sha256: str = ''
73
+ timestamp: int = 0
74
+ sig: str = ''
75
+
76
+ def compute_sha256(self) -> str:
77
+ return hashlib.sha256(self.payload).hexdigest()
78
+
79
+ def signing_digest(self) -> bytes:
80
+ meta = {
81
+ 'stream_id': self.stream_id,
82
+ 'broadcaster': self.broadcaster.lower(),
83
+ 'group_id': int(self.group_id),
84
+ 'object_id': int(self.object_id),
85
+ 'track': self.track,
86
+ 'is_keyframe': bool(self.is_keyframe),
87
+ 'is_audio_only': bool(self.is_audio_only),
88
+ 'sha256': self.sha256.lower() or self.compute_sha256(),
89
+ 'timestamp': int(self.timestamp),
90
+ }
91
+ return hashlib.sha256(crypto.canonical(meta)).digest()
92
+
93
+ def sign(self, sk_hex: str) -> 'MoQFrame':
94
+ digest_hex = self.sha256.lower() or self.compute_sha256()
95
+ with_hash = dataclasses.replace(self, sha256=digest_hex)
96
+ sig_hex = crypto.schnorr_sign(with_hash.signing_digest(), sk_hex)
97
+ return dataclasses.replace(with_hash, sig=sig_hex)
98
+
99
+ def verify_integrity(self) -> bool:
100
+ """Verify payload SHA-256 and broadcaster BIP-340 Schnorr signature.
101
+ Required security boundary for unauthenticated UDP rungs (IrohLiveEngine).
102
+ """
103
+ expected_hash = self.compute_sha256()
104
+ if self.sha256 and self.sha256.lower() != expected_hash:
105
+ return False
106
+ if not self.sig or not self.broadcaster or len(self.sig) != 128:
107
+ return False
108
+ try:
109
+ return crypto.schnorr_verify(self.signing_digest(), self.broadcaster.lower(), self.sig)
110
+ except (ValueError, TypeError):
111
+ return False
112
+
113
+ def to_cbor(self) -> bytes:
114
+ d: MoQFrameDict = {
115
+ 'stream_id': self.stream_id,
116
+ 'broadcaster': self.broadcaster.lower(),
117
+ 'group_id': int(self.group_id),
118
+ 'object_id': int(self.object_id),
119
+ 'track': self.track,
120
+ 'is_keyframe': bool(self.is_keyframe),
121
+ 'is_audio_only': bool(self.is_audio_only),
122
+ 'payload': self.payload,
123
+ 'sha256': self.sha256.lower() or self.compute_sha256(),
124
+ 'timestamp': int(self.timestamp),
125
+ 'sig': self.sig.lower(),
126
+ }
127
+ return cbor_encode(d, canonical=True)
128
+
129
+ @classmethod
130
+ def from_cbor(cls, data: bytes) -> 'MoQFrame':
131
+ decoded, _ = cbor_decode(data)
132
+ if not isinstance(decoded, Mapping):
133
+ raise ValueError('Decoded MoQFrame CBOR is not a mapping')
134
+ payload_raw = decoded.get('payload', b'')
135
+ payload_bytes = bytes(payload_raw) if isinstance(payload_raw, (bytes, bytearray)) else b''
136
+ return cls(
137
+ stream_id=str(decoded.get('stream_id', '')),
138
+ broadcaster=str(decoded.get('broadcaster', '')).lower(),
139
+ group_id=int(decoded.get('group_id', 0)),
140
+ object_id=int(decoded.get('object_id', 0)),
141
+ track=str(decoded.get('track', 'video')),
142
+ is_keyframe=bool(decoded.get('is_keyframe', False)),
143
+ is_audio_only=bool(decoded.get('is_audio_only', False)),
144
+ payload=payload_bytes,
145
+ sha256=str(decoded.get('sha256', '')).lower(),
146
+ timestamp=int(decoded.get('timestamp', 0)),
147
+ sig=str(decoded.get('sig', '')).lower(),
148
+ )
149
+
150
+
151
+ class TransitReflectorManager:
152
+ """Manages automated capacity shed handovers, MoQ ring-buffer reflection,
153
+ Rhio NATS JetStream cluster fanout, and bilateral transit metering.
154
+ """
155
+
156
+ def __init__(
157
+ self,
158
+ ledger: Optional[TransitLedgerStore] = None,
159
+ packager: Optional[VideoStreamPackager] = None,
160
+ ring_capacity: int = 64,
161
+ rhio_client: Optional[Any] = None,
162
+ fips_transport: Optional[Any] = None,
163
+ iroh_engine: Optional[Any] = None,
164
+ clock: Optional[Callable[[], int]] = None,
165
+ strain_ledger: Optional[Any] = None,
166
+ ) -> None:
167
+ # An injected clock, defaulting to wall time, so a suite can move time
168
+ # at the boundaries this code decides on. The compartment passes its own.
169
+ self._clock: Callable[[], int] = clock or (lambda: int(time.time()))
170
+ # MoQ frames a transport would not take, per transport. A dropped
171
+ # frame in live video is ordinary and not worth a refusal each, but
172
+ # these were discarded entirely, so a subscriber whose transport was
173
+ # wholly broken looked exactly like one receiving everything.
174
+ self.frames_dropped: dict[str, int] = {}
175
+ self.ledger = ledger or TransitLedgerStore(':memory:')
176
+ self.packager = packager
177
+ self.ring_capacity = max(4, int(ring_capacity))
178
+ self.rhio_client = rhio_client
179
+ self.fips_transport = fips_transport
180
+ self.iroh_engine = iroh_engine
181
+ self.strain_ledger = strain_ledger
182
+
183
+ self._lock = threading.RLock()
184
+ # stream_id -> active TransitGrantBody
185
+ self._active_handovers: dict[str, TransitGrantBody] = {}
186
+ # stream_id -> bounded drop-on-congestion ring buffer of MoQFrames
187
+ self._rings: dict[str, Deque[MoQFrame]] = {}
188
+ # grant_id -> cumulative transit_bytes_out metered by reflector
189
+ self._metered_bytes: dict[str, int] = {}
190
+
191
+ # ---- Handshake Helpers --------------------------------------------------
192
+
193
+ def build_solicitation(
194
+ self,
195
+ stream_id: str,
196
+ broadcaster: str,
197
+ bitrate_kbps: int = 4500,
198
+ max_transit_bytes: int = 2 * 1024 * 1024 * 1024,
199
+ mode: TransitMode = 'pro-bono',
200
+ codecs: Sequence[str] = ('avc1.64001f', 'mp4a.40.2'),
201
+ now: Optional[int] = None,
202
+ ) -> TransitSolicitationBody:
203
+ now_val = self._clock() if now is None else int(now)
204
+ return {
205
+ 'stream_id': stream_id,
206
+ 'broadcaster': broadcaster.lower(),
207
+ 'bitrate_kbps': int(bitrate_kbps),
208
+ 'max_transit_bytes': int(max_transit_bytes),
209
+ 'mode': mode,
210
+ 'codecs': list(codecs),
211
+ 'at': now_val,
212
+ }
213
+
214
+ def accept_solicitation(
215
+ self,
216
+ solicitation: TransitSolicitationBody,
217
+ operator_pk: str,
218
+ operator_sk: str,
219
+ reflector_endpoints: Sequence[str],
220
+ window_seconds: int = 600,
221
+ receipt_cadence_seconds: int = 60,
222
+ max_unsettled_transit_bytes: int = 500 * 1024 * 1024,
223
+ now: Optional[int] = None,
224
+ ) -> TransitGrantBody:
225
+ """Operator supernode accepts a transit solicitation and issues a signed grant."""
226
+ return self.ledger.issue_grant(
227
+ stream_id=solicitation['stream_id'],
228
+ broadcaster=solicitation['broadcaster'],
229
+ operator_pk=operator_pk,
230
+ operator_sk=operator_sk,
231
+ window_seconds=window_seconds,
232
+ receipt_cadence_seconds=receipt_cadence_seconds,
233
+ max_unsettled_transit_bytes=max_unsettled_transit_bytes,
234
+ reflector_endpoints=reflector_endpoints,
235
+ mode=solicitation.get('mode', 'pro-bono'),
236
+ now=now,
237
+ )
238
+
239
+ # ---- Capacity Shed Ladder Handover Automation ---------------------------
240
+
241
+ def is_handover_active(self, stream_id: str) -> bool:
242
+ """Return True if `stream_id` currently has an active transit reflector handover."""
243
+ with self._lock:
244
+ return stream_id in self._active_handovers
245
+
246
+ def active_sessions(self) -> dict[str, TransitGrantBody]:
247
+ """Return a snapshot of active stream_id -> TransitGrantBody handovers."""
248
+ with self._lock:
249
+ return dict(self._active_handovers)
250
+
251
+ def engage_handover(self, stream_id: str, grant: TransitGrantBody) -> list[str]:
252
+ """Engage transit handover for `stream_id` using `grant`.
253
+ Updates VideoStreamPackager's active reflector endpoints so HTTP requests
254
+ receive 307 Temporary Redirects and manifests advertise the reflector.
255
+ """
256
+ self.ledger.save_grant(grant, status='live')
257
+ endpoints = list(grant.get('reflector_endpoints', []))
258
+ with self._lock:
259
+ self._active_handovers[stream_id] = grant
260
+ if self.packager is not None:
261
+ try:
262
+ self.packager.update_transit_endpoints(stream_id, endpoints)
263
+ except KeyError:
264
+ pass
265
+ return endpoints
266
+
267
+ def release_handover(
268
+ self,
269
+ stream_id: str,
270
+ window: int,
271
+ sk_hex: str,
272
+ reason: str = 'capacity-restored',
273
+ now: Optional[int] = None,
274
+ ) -> Optional[ tuple[TransitReceiptBody, TransitCloseBody]]:
275
+ """Release transit handover once Capacity pressure drops below `restore_below`.
276
+ Emits a final cumulative TransitReceiptBody and TransitCloseBody, and
277
+ clears redirect endpoints on the origin packager.
278
+ """
279
+ now_val = self._clock() if now is None else int(now)
280
+ with self._lock:
281
+ grant = self._active_handovers.pop(stream_id, None)
282
+ if grant is None:
283
+ return None
284
+ grant_id = grant['grant_id']
285
+ total_bytes = self._metered_bytes.get(grant_id, 0)
286
+
287
+ receipt, head = self.ledger.record_outbound_receipt(
288
+ grant_id=grant_id,
289
+ stream_id=stream_id,
290
+ window=window,
291
+ transit_bytes_out=total_bytes,
292
+ sk_hex=sk_hex,
293
+ now=now_val,
294
+ )
295
+ close_body: TransitCloseBody = {
296
+ 'grant': grant_id,
297
+ 'stream_id': stream_id,
298
+ 'window': int(window),
299
+ 'final_receipt_head': head,
300
+ 'counters': {COUNTER_TRANSIT_BYTES_OUT: total_bytes},
301
+ 'reason': reason,
302
+ 'at': now_val,
303
+ }
304
+ self.ledger.close_grant(grant_id)
305
+ with self._lock:
306
+ if not hasattr(self, '_last_release'):
307
+ self._last_release: dict[str, tuple[TransitReceiptBody, TransitCloseBody]] = {}
308
+ self._last_release[stream_id] = (receipt, close_body)
309
+ if self.packager is not None:
310
+ try:
311
+ self.packager.update_transit_endpoints(stream_id, ())
312
+ except KeyError:
313
+ pass
314
+ return receipt, close_body
315
+
316
+ def evaluate_capacity_handover(
317
+ self,
318
+ capacity_comp: Any,
319
+ stream_id: str,
320
+ standby_grant: TransitGrantBody,
321
+ window: int,
322
+ sk_hex: str,
323
+ now: Optional[int] = None,
324
+ ) -> str:
325
+ """Inspect the node's Capacity compartment shed state (`narrow_scope` /
326
+ `amend_down`) and automatically engage or release transit handover.
327
+ Returns 'engaged', 'released', 'active', or 'idle'.
328
+ """
329
+ shed_active = bool(
330
+ (hasattr(capacity_comp, 'narrowing') and capacity_comp.narrowing())
331
+ or (hasattr(capacity_comp, 'amending') and capacity_comp.amending())
332
+ )
333
+ with self._lock:
334
+ currently_handed_over = stream_id in self._active_handovers
335
+
336
+ if shed_active and not currently_handed_over:
337
+ self.engage_handover(stream_id, standby_grant)
338
+ return 'engaged'
339
+ elif not shed_active and currently_handed_over:
340
+ self.release_handover(stream_id, window=window, sk_hex=sk_hex, now=now)
341
+ return 'released'
342
+ return 'active' if currently_handed_over else 'idle'
343
+
344
+ # ---- Real-Time MoQ Ring Buffer Reflection & Cluster Fanout --------------
345
+
346
+ def get_ring_frames(self, stream_id: str) -> list[MoQFrame]:
347
+ with self._lock:
348
+ ring = self._rings.get(stream_id)
349
+ return list(ring) if ring is not None else []
350
+
351
+ def reflect_moq_frame(
352
+ self,
353
+ frame: MoQFrame,
354
+ grant_id: Optional[str] = None,
355
+ subscribers: Sequence[str] = (),
356
+ ) -> ReflectionResult:
357
+ """Verify, buffer, and fan out a real-time MoQ frame across FIPS (port 30820),
358
+ Iroh UDP streams, and Rhio NATS JetStream cluster subjects.
359
+ """
360
+ # 1. Cryptographic integrity check at the ingress door
361
+ if not frame.verify_integrity():
362
+ if self.strain_ledger is not None and hasattr(self.strain_ledger, 'record'):
363
+ self.strain_ledger.record('invalid_moq_frame', peer=frame.broadcaster)
364
+ return {
365
+ 'accepted': False,
366
+ 'forwarded': False,
367
+ 'degraded_audio_only': False,
368
+ 'dropped_reason': 'invalid_frame_signature_or_digest',
369
+ 'bytes_fanned_out': 0,
370
+ 'ring_depth': len(self.get_ring_frames(frame.stream_id)),
371
+ 'rhio_published': False,
372
+ }
373
+
374
+ # 2. Check soft debt ceiling (`max_unsettled_transit_bytes`)
375
+ summary = self.ledger.debt_summary(frame.broadcaster, grant_id=grant_id)
376
+ degraded = summary['degraded_audio_only']
377
+ if degraded and not frame.is_audio_only:
378
+ return {
379
+ 'accepted': True,
380
+ 'forwarded': False,
381
+ 'degraded_audio_only': True,
382
+ 'dropped_reason': 'soft_debt_ceiling_audio_only_degradation',
383
+ 'bytes_fanned_out': 0,
384
+ 'ring_depth': len(self.get_ring_frames(frame.stream_id)),
385
+ 'rhio_published': False,
386
+ }
387
+
388
+ # 3. Append to bounded drop-on-congestion ring buffer
389
+ with self._lock:
390
+ ring = self._rings.get(frame.stream_id)
391
+ if ring is None:
392
+ ring = collections.deque(maxlen=self.ring_capacity)
393
+ self._rings[frame.stream_id] = ring
394
+ ring.append(frame)
395
+ ring_depth = len(ring)
396
+
397
+ # 4. Meter outbound bytes across subscribers
398
+ fanout_multiplier = max(1, len(subscribers))
399
+ bytes_fanned = len(frame.payload) * fanout_multiplier
400
+ if grant_id is not None:
401
+ with self._lock:
402
+ self._metered_bytes[grant_id] = self._metered_bytes.get(grant_id, 0) + bytes_fanned
403
+
404
+ # 5. Fan out to FIPS native FSP port 30820 and Iroh UDP engine if attached
405
+ cbor_frame = frame.to_cbor()
406
+ for sub_peer in subscribers:
407
+ if self.fips_transport is not None and hasattr(self.fips_transport, 'send_datagram'):
408
+ try:
409
+ self.fips_transport.send_datagram(sub_peer, cbor_frame, port=PORT_VIDEO_STREAM)
410
+ except Exception:
411
+ self.frames_dropped['fips'] = self.frames_dropped.get('fips', 0) + 1
412
+ if self.iroh_engine is not None and hasattr(self.iroh_engine, 'send_stream_chunk'):
413
+ try:
414
+ self.iroh_engine.send_stream_chunk(sub_peer, cbor_frame)
415
+ except Exception:
416
+ self.frames_dropped['iroh'] = self.frames_dropped.get('iroh', 0) + 1
417
+
418
+ # 6. Fan out to Rhio NATS JetStream cluster (`fleetmesh.video.<stream_id>`)
419
+ rhio_published = False
420
+ if self.rhio_client is not None and hasattr(self.rhio_client, 'publish'):
421
+ try:
422
+ res = self.rhio_client.publish(
423
+ f"fleetmesh.video.{frame.stream_id}",
424
+ {
425
+ 'stream_id': frame.stream_id,
426
+ 'group_id': frame.group_id,
427
+ 'object_id': frame.object_id,
428
+ 'track': frame.track,
429
+ 'is_audio_only': frame.is_audio_only,
430
+ 'sha256': frame.sha256,
431
+ 'size': len(frame.payload),
432
+ 'timestamp': frame.timestamp,
433
+ },
434
+ )
435
+ rhio_published = bool(res.get('ok', True)) if isinstance(res, Mapping) else True
436
+ except Exception:
437
+ rhio_published = False
438
+
439
+ return {
440
+ 'accepted': True,
441
+ 'forwarded': True,
442
+ 'degraded_audio_only': degraded,
443
+ 'dropped_reason': '',
444
+ 'bytes_fanned_out': bytes_fanned,
445
+ 'ring_depth': ring_depth,
446
+ 'rhio_published': rhio_published,
447
+ }
448
+
449
+ def emit_metered_receipt(
450
+ self,
451
+ grant_id: str,
452
+ stream_id: str,
453
+ window: int,
454
+ sk_hex: str,
455
+ now: Optional[int] = None,
456
+ ) -> tuple[TransitReceiptBody, str]:
457
+ """Produce and persist a signed cumulative TransitReceiptBody for all
458
+ bytes metered under `grant_id` so far.
459
+ """
460
+ with self._lock:
461
+ total = self._metered_bytes.get(grant_id, 0)
462
+ return self.ledger.record_outbound_receipt(
463
+ grant_id=grant_id,
464
+ stream_id=stream_id,
465
+ window=window,
466
+ transit_bytes_out=total,
467
+ sk_hex=sk_hex,
468
+ now=now,
469
+ )
470
+
471
+
472
+ __all__ = [
473
+ 'MoQFrameDict',
474
+ 'ReflectionResult',
475
+ 'MoQFrame',
476
+ 'TransitReflectorManager',
477
+ 'receipt_link',
478
+ ]
@@ -0,0 +1,24 @@
1
+ """Store plane: sync, retention, projected, blob_wire, blob_replication."""
2
+ from . import blob_replication, blob_wire, projected, retention, sync
3
+ from .blob_replication.compartment import Replication
4
+ from .blob_wire.core import BlobWire, BlobWireConfig
5
+ from .projected.compartment import ProjectedDB, ProjectedDBConfig
6
+ from .retention.compartment import Retention, RetentionConfig
7
+ from .sync.core import SyncConfig, SyncPeer
8
+
9
+ __all__ = [
10
+ 'BlobWire',
11
+ 'BlobWireConfig',
12
+ 'ProjectedDB',
13
+ 'ProjectedDBConfig',
14
+ 'Replication',
15
+ 'Retention',
16
+ 'RetentionConfig',
17
+ 'SyncConfig',
18
+ 'SyncPeer',
19
+ 'blob_replication',
20
+ 'blob_wire',
21
+ 'projected',
22
+ 'retention',
23
+ 'sync',
24
+ ]
@@ -0,0 +1,7 @@
1
+ """Blossom replication: keep N copies of every pinned blob on other nodes.
2
+
3
+ One-module compartment: everything is in compartment.py.
4
+ """
5
+ from .compartment import Replication
6
+
7
+ __all__ = ['Replication']