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,128 @@
1
+ """The initiator: `pull` runs one sync/1.0 session against a peer under the
2
+ grant it issued us, keeps the evidence a report could carry, and never
3
+ files a finding for a peer that merely went quiet."""
4
+ from __future__ import annotations
5
+
6
+ from typing import Any, Mapping
7
+
8
+ from fleetmesh_core.storage import store, sync
9
+ from node.storage import grants as grants_mod
10
+ from .envelope import _thid, verified_events
11
+ from .shape import SyncSessionResult, SyncShape
12
+
13
+
14
+ class InitiatorMixin:
15
+ # ---- initiator ---------------------------------------------------------
16
+ def pull(
17
+ self: SyncShape,
18
+ peer_pk: str,
19
+ shard: str,
20
+ interest: Mapping[str, Any],
21
+ since: int = 0,
22
+ until: int = 2 ** 31,
23
+ timeout: float = 30.0,
24
+ max_messages: int = 64,
25
+ ) -> SyncSessionResult:
26
+ """Reconcile one shard with a peer under the grant it issued us."""
27
+ grant = self._grant(peer_pk, grants_mod.RECEIVED)
28
+ sess = sync.Session(self.store, grant, shard, interest, since, until, self.clock(), is_initiator=True)
29
+ sess.thid = _thid(shard)
30
+ self._initiated.add(sess.thid) # the oldest forgotten at the store's bound
31
+ q = self.transport.listen(sess.thid)
32
+ evidence: list[Mapping[str, Any]] = []
33
+ status: str = 'aborted'
34
+ try:
35
+ m = sess.open()
36
+ for _ in range(max_messages):
37
+ self._send(m, peer_pk)
38
+ try:
39
+ inbound = q.get(timeout=timeout)
40
+ except Exception:
41
+ # No finding. A peer that stops answering mid-session has
42
+ # told us nothing about its honesty: the specification's own
43
+ # partition rule is that a dead node and a quiet one cannot
44
+ # be told apart, and that a peer MUST NOT be reported
45
+ # unreachable until every shared transport rung has failed.
46
+ # This filed an S2, so an operator restarting their node
47
+ # earned a misreport finding at every peer that happened to
48
+ # be mid-sync - ordinary operations priced as misbehaviour.
49
+ # Whatever traffic did move is still measured, and if it
50
+ # went unaccounted the window closes on it as active
51
+ # silence, which is the mechanism that exists for this.
52
+ self._record(self.aborted, (peer_pk, sess.thid))
53
+ # A peer that stopped answering is a partition, never a
54
+ # finding - the specification's own rule. Recorded so the
55
+ # node is readier for the next one rather than discarded.
56
+ #
57
+ # Unless the peer said it was going. A reserve is a
58
+ # function of the directions stress came from, and a
59
+ # departure this node was told about in advance is not
60
+ # stress from that direction: thickening against it would
61
+ # make an orderly retirement cost its peers exactly what
62
+ # the retirement exists to save them.
63
+ presence = self.presence
64
+ announced = presence is not None and presence.is_retiring(peer_pk)
65
+ if self.strain is not None and not announced:
66
+ self.strain.record('partition', peer=peer_pk, shard=getattr(sess, 'shard', None))
67
+ break
68
+ reply = inbound.message
69
+ self._seen(peer_pk, reply)
70
+ kind = reply['type'].rsplit('/', 1)[1]
71
+ if kind == 'done':
72
+ status = reply['body'].get('status')
73
+ break
74
+ if kind == 'deliver':
75
+ events, envelopes = verified_events(reply, sess.findings)
76
+ reply = dict(reply, body=dict(reply['body'], events=events, count=len(events)))
77
+ # What the reference will refuse as outside the interest is
78
+ # the evidence a report carries, so keep it before it does -
79
+ # with its envelope, which the sender signed even when it
80
+ # did not write the event.
81
+ evidence += [{'event': e, 'envelope': envelopes.get(e['id'])}
82
+ for e in events if not store.matches(e, sess.interest)]
83
+ if kind not in ('range', 'deliver'):
84
+ sess._finding('S2', 'unexpected sync message %r' % kind)
85
+ break
86
+ if kind == 'deliver' and hasattr(self.store, 'note_origin'):
87
+ for e in reply['body']['events']:
88
+ self.store.note_origin(e['id'], self.store.MESH)
89
+ m = getattr(sess, 'on_' + kind)(reply)
90
+ if kind == 'deliver' and sess.forged:
91
+ evidence += [{'event': e, 'envelope': envelopes.get(e.get('id'))} for e in sess.forged]
92
+ if m['type'].endswith('/done'):
93
+ self._send(m, peer_pk)
94
+ status = m['body'].get('status')
95
+ break
96
+ else:
97
+ sess._finding('S2', 'session did not terminate in %d messages' % max_messages)
98
+ status = 'aborted'
99
+ finally:
100
+ self.transport.unlisten(sess.thid)
101
+ # A handover follows the done on this same thread, and it can
102
+ # arrive in the gap between the done going out and the listener
103
+ # coming down. Anything still queued there is unsolicited now,
104
+ # so it goes where unsolicited messages go.
105
+ while True:
106
+ try:
107
+ late = q.get_nowait()
108
+ except Exception:
109
+ break
110
+ self.transport.inbox.put(late)
111
+ result: SyncSessionResult = {
112
+ 'status': status,
113
+ 'counters': dict(sess.counters),
114
+ 'findings': list(sess.findings),
115
+ 'need': sorted(getattr(sess, 'need', [])),
116
+ 'dup_ratio': sess.dup_ratio(),
117
+ 'thid': sess.thid,
118
+ 'evidence': evidence,
119
+ 'grant': grant.event_id,
120
+ }
121
+ with self._lock:
122
+ self._record(self.completed, (peer_pk, 'initiator', result))
123
+ if self.budget is not None:
124
+ self.budget.observe_session(peer_pk, 'initiator', result)
125
+ for cb in self.on_session:
126
+ cb(peer_pk, 'initiator', result)
127
+ return result
128
+
@@ -0,0 +1,114 @@
1
+ """The responder: `handle` takes one inbound sync/1.0 message, enforces the
2
+ grant (not just measures it), bounds sessions per peer, and answers in the
3
+ wire form - events in the signed attachment, the count in the body."""
4
+ from __future__ import annotations
5
+
6
+ import os
7
+ from typing import Any, Mapping, Optional
8
+
9
+ from fleetmesh_core.wire import validate
10
+ from node.storage import grants as grants_mod
11
+ from .envelope import MAX_SESSIONS_PER_PEER, PIURI, SyncError, envelope_attachment
12
+ from .shape import SyncShape
13
+
14
+
15
+ class ResponderMixin:
16
+ def _refuse_open(self: SyncShape, peer_pk: str, thid: Optional[str], why: str) -> tuple[Any, ...]:
17
+ """Refuse a session with a done, so the peer stops waiting, and say why."""
18
+ self._record(self.refused, (peer_pk, thid, why))
19
+ self.refusals.refuse('session', why, subject=peer_pk)
20
+ reply = {'id': 'sync-refuse-%s' % os.urandom(4).hex(), 'type': PIURI + '/done', 'thid': thid,
21
+ 'body': {'status': 'aborted', 'events_transferred': 0}}
22
+ self._send(reply, peer_pk)
23
+ return ('refused', peer_pk, why)
24
+
25
+ def _precheck(self: SyncShape, inbound: Any, kind: str) -> tuple[Optional[Mapping[str, Any]], str, Optional[tuple[Any, ...]]]:
26
+ msg = getattr(inbound, 'message', inbound)
27
+ peer_pk = getattr(inbound, 'sender', msg.get('from', '') if isinstance(msg, dict) else '')
28
+ self._seen(peer_pk, msg)
29
+ why = validate.check_body('sync', kind, msg.get('body'))
30
+ if why is not None:
31
+ self.rejected.append((peer_pk, kind, why))
32
+ return None, peer_pk, ('ignored', peer_pk, 'malformed %s: %s' % (kind, why))
33
+ self._expire(self.clock())
34
+ return msg, peer_pk, None
35
+
36
+ def _send_reply(self: SyncShape, thid: Optional[str], peer_pk: str, sess: Any, reply: Mapping[str, Any]) -> tuple[Any, ...]:
37
+ if reply['type'].endswith('/deliver'):
38
+ events = reply['body']['events']
39
+ reply = dict(reply, body={'count': len(events)},
40
+ attachments=[envelope_attachment(events, self.ks.did,
41
+ self.ks.node_sk, self.clock())])
42
+ if reply['type'].endswith('/done'):
43
+ status = reply['body'].get('status')
44
+ self._finish(thid or '', peer_pk, sess, status)
45
+ self._send(reply, peer_pk)
46
+ offered = self._offer_handover(thid or '', peer_pk, sess, status)
47
+ return ('done', peer_pk, status) + ((offered,) if offered else ())
48
+ self._send(reply, peer_pk)
49
+ return ('replied', peer_pk, reply['type'].rsplit('/', 1)[1])
50
+
51
+ def handle_handover(self: SyncShape, inbound: Any, sender: str) -> Optional[tuple[Any, ...]]:
52
+ msg, peer_pk, err = self._precheck(inbound, 'handover')
53
+ if err is not None or msg is None:
54
+ return err
55
+ return self._accept_handover(peer_pk, msg['body'])
56
+
57
+ def handle_open(self: SyncShape, inbound: Any, sender: str) -> Optional[tuple[Any, ...]]:
58
+ msg, peer_pk, err = self._precheck(inbound, 'open')
59
+ if err is not None or msg is None:
60
+ return err
61
+ thid = str(msg.get('thid') or '')
62
+ try:
63
+ grant = self._grant(peer_pk, grants_mod.ISSUED)
64
+ except SyncError as e:
65
+ return ('ignored', peer_pk, str(e))
66
+ body = msg['body']
67
+ with self._lock:
68
+ mine = sum(1 for v in self.sessions.values() if v[0] == peer_pk)
69
+ if mine >= MAX_SESSIONS_PER_PEER:
70
+ return self._refuse_open(peer_pk, thid, 'too many open sessions')
71
+ refuse_why = self.budget.refuses(peer_pk) if self.budget is not None else None
72
+ if refuse_why is not None:
73
+ return self._refuse_open(peer_pk, thid, refuse_why)
74
+ sess = self.session_cls(self.store, grant, body['shard'], body.get('filter'), body.get('since', 0),
75
+ body.get('until', 2 ** 31), self.clock(), is_initiator=False)
76
+ sess.thid = thid
77
+ with self._lock:
78
+ kept = self.sessions.put(thid, (peer_pk, sess, self.clock()))
79
+ if not kept:
80
+ return self._refuse_open(peer_pk, thid, 'this node has too many open sessions')
81
+ reply = sess.on_open(msg)
82
+ return self._send_reply(thid, peer_pk, sess, reply)
83
+
84
+ def _dispatch_session(self: SyncShape, inbound: Any, kind: str) -> Optional[tuple[Any, ...]]:
85
+ msg, peer_pk, err = self._precheck(inbound, kind)
86
+ if err is not None or msg is None:
87
+ return err
88
+ thid = str(msg.get('thid') or '')
89
+ if not thid:
90
+ return ('ignored', peer_pk, 'no such session')
91
+ with self._lock:
92
+ entry = self.sessions.get(thid)
93
+ if entry is not None:
94
+ self.sessions[thid] = (entry[0], entry[1], self.clock())
95
+ if entry is None or entry[0] != peer_pk:
96
+ return ('ignored', peer_pk, 'no such session')
97
+ sess = entry[1]
98
+ if kind == 'done':
99
+ status = msg['body'].get('status')
100
+ self._finish(thid or '', peer_pk, sess, status)
101
+ offered = self._offer_handover(thid or '', peer_pk, sess, status)
102
+ return ('done', peer_pk, status) + ((offered,) if offered else ())
103
+ reply = getattr(sess, 'on_' + kind)(msg)
104
+ return self._send_reply(thid, peer_pk, sess, reply)
105
+
106
+ def handle_range(self: SyncShape, inbound: Any, sender: str) -> Optional[tuple[Any, ...]]:
107
+ return self._dispatch_session(inbound, 'range')
108
+
109
+ def handle_need(self: SyncShape, inbound: Any, sender: str) -> Optional[tuple[Any, ...]]:
110
+ return self._dispatch_session(inbound, 'need')
111
+
112
+ def handle_done(self: SyncShape, inbound: Any, sender: str) -> Optional[tuple[Any, ...]]:
113
+ return self._dispatch_session(inbound, 'done')
114
+
@@ -0,0 +1,179 @@
1
+ """What this node will sign for, and what it will take somebody's word on.
2
+
3
+ The handover is the one shape of `asserted` a node may publish over a set it
4
+ did not receive first-hand, and acting on one touches two compartments: the
5
+ sync wire, which offers and accepts them over the wire, and discovery, which
6
+ publishes the claim that rests on one. The rules were written in both places
7
+ and a third copy sat in `validate`, so "what makes a handover acceptable" had
8
+ three homes and no single answer.
9
+
10
+ They live here, as decisions over plain data. Nothing in this module signs,
11
+ sends, stores or refuses: the compartments keep all of that, because the key,
12
+ the transport, the account store and the refusal channel are theirs. What
13
+ they get back is a verdict and a reason for it, and the reason is what the
14
+ operator reads.
15
+
16
+ `fleetmesh_core/coverage.py` owns the artefact - its shape, its signature, and the chain
17
+ rules every reader applies. This owns the part that is about *this* node: the
18
+ shard it was configured to hold, the set on its own shelves, the session that
19
+ just closed, and the operator's `handover` setting.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ from typing import Any, Mapping, Optional, Sequence, cast
25
+
26
+ from fleetmesh_core.wire.wire_types import HandoverLink
27
+ from fleetmesh_core.peering import coverage
28
+ from fleetmesh_core.storage import replication
29
+ from .types import CoverageShardConfig
30
+
31
+
32
+ def canonical(filt: Any) -> str:
33
+ """A filter as the artefact carries it. Two filters that differ only in
34
+ key order are one filter, and a handover that compared them as written
35
+ would refuse a peer for having sorted its JSON differently."""
36
+ return json.dumps(filt, sort_keys=True, separators=(',', ':'))
37
+
38
+
39
+ # The shape check for a handover body is core's, `validate.check_handover_body`,
40
+ # which the sync responder already applies through validate.check_body. A copy
41
+ # of it lived here, unused by anything, with the boolean-as-integer hole core
42
+ # has since closed (audit 2026-10-15, D1 and B9).
43
+
44
+
45
+ def offered_set(
46
+ claim: Optional[Mapping[str, Any]],
47
+ session: Any,
48
+ now: int,
49
+ setting: str = 'offer',
50
+ status: Any = 'complete',
51
+ ) -> tuple[Optional[str], Optional[str]]:
52
+ """(fingerprint, why_not) for a session that may have earned a handover.
53
+
54
+ Every condition is about what this node has already said in public. The
55
+ claim is the kind 30803 last published: asserted, with a bounded range
56
+ that has closed, and the session covered the whole of that range under
57
+ that same filter, so the peer now holds the set the claim is about. A node
58
+ that published best-effort, or whose range is still open, has nothing it
59
+ can sign for.
60
+
61
+ `why_not` is for the operator and not for the wire. Most of these are the
62
+ ordinary case - a session that reconciled part of a shard has earned
63
+ nothing and there is nothing wrong with that - so a caller logs at most
64
+ the ones it asked about.
65
+ """
66
+ if status not in ('complete', 'in_sync'):
67
+ return None, 'the session ended %s' % status
68
+ if setting != 'offer':
69
+ return None, 'the operator does not offer handovers'
70
+ if claim is None or not coverage.reportable(dict(claim)):
71
+ return None, 'this node publishes no asserted claim for the shard'
72
+ from fleetmesh_core.storage import store as ref_store
73
+ until = ref_store.first_tag(claim, 'until')
74
+ if not until or len(until) < 2:
75
+ return None, 'the claim has no until: a handover signs for a closed set'
76
+ filt = ref_store.first_tag(claim, 'filter')
77
+ fp = ref_store.first_tag(claim, 'fingerprint')
78
+ if not filt or len(filt) < 2 or not fp or len(fp) < 2:
79
+ # An asserted claim always carries both; one without them was not
80
+ # written by fleetmesh_core.peering.coverage and nothing can be signed for it.
81
+ return None, 'the claim carries no filter or fingerprint'
82
+ since_tag = ref_store.first_tag(claim, 'since')
83
+ c_since = int(since_tag[1]) if since_tag and len(since_tag) > 1 else 0
84
+ c_until = int(until[1])
85
+ if c_until > now:
86
+ return None, 'the claim range has not closed yet'
87
+ if session.since > c_since or session.until < c_until:
88
+ return None, 'the session did not cover the whole of the claim range'
89
+ if canonical(session.interest) != canonical(json.loads(filt[1])):
90
+ return None, 'the session ran under a different filter'
91
+ return fp[1], None
92
+
93
+
94
+ def extend(held: Sequence[HandoverLink | Mapping[str, Any]], link: HandoverLink | Mapping[str, Any]) -> tuple[Optional[list[HandoverLink]], Optional[str]]:
95
+ """(chain_to_send, why_not): what this node holds, plus its own link.
96
+
97
+ A node passing on a shard it holds under a handover passes on the whole
98
+ provenance with it. Carrying only its own link would leave the node that
99
+ withheld an event able to report the hole against the node below, liable
100
+ for it and unbarred from reporting it. The chain is checked before it
101
+ goes: one this node cannot stand behind is one it should not sign onto.
102
+ """
103
+ chain = [dict(h) for h in held] + [dict(link)]
104
+ if len(chain) > coverage.HANDOVER_MAX_CHAIN:
105
+ return None, 'the chain would be %d links, longer than %d' % (len(chain), coverage.HANDOVER_MAX_CHAIN)
106
+ problems = coverage.check_chain(chain)
107
+ if problems:
108
+ return None, problems[0]
109
+ return cast(list[HandoverLink], chain), None
110
+
111
+
112
+ def acceptable(
113
+ chain: Sequence[Mapping[str, Any]],
114
+ me: str,
115
+ sender: str,
116
+ shard_config: Optional[Mapping[str, Any]],
117
+ held_ids: Optional[Sequence[str]],
118
+ ) -> Optional[str]:
119
+ """Why this node must not keep an inbound chain, or None to keep it.
120
+
121
+ Held to more than its signatures. Every link must verify and the chain
122
+ must join up and describe one set, which is `fleetmesh_core/coverage.py`'s business.
123
+ Then: the last link names this node, it was signed by the peer that sent
124
+ it, the shard is one the operator configured with that same filter, and
125
+ the set fingerprints to what this node holds over the handed range right
126
+ now. A chain over any other set could only ever support a false claim,
127
+ and refusing it costs less than publishing one and dropping it next tick.
128
+
129
+ `held_ids` is what this node holds over the handed range, or None when the
130
+ caller could not look - the shard is unconfigured and the answer is
131
+ already no.
132
+ """
133
+ if not chain:
134
+ return 'the chain is empty'
135
+ problems: list[str] = []
136
+ for link in chain:
137
+ problems += coverage.check_handover(link)
138
+ problems += coverage.check_chain(chain)
139
+ if problems:
140
+ return problems[0]
141
+ art = chain[-1]
142
+ if art['to'] != me:
143
+ return 'names %s, not this node' % art['to'][:16]
144
+ if art['from'] != sender:
145
+ return 'signed by %s and sent by %s' % (art['from'][:16], sender[:16])
146
+ if shard_config is None:
147
+ return 'shard %s is not one this node claims' % art['shard']
148
+ if canonical(shard_config.get('filter')) != art['filter']:
149
+ return 'the handed filter is not the one configured for %s' % art['shard']
150
+ if held_ids is None:
151
+ return 'this node cannot say what it holds for %s' % art['shard']
152
+ if replication.fingerprint(held_ids) != art['fingerprint'] or len(held_ids) != art['count']:
153
+ return ('what this node holds over the handed range is not the handed set (%d held, %d handed)'
154
+ % (len(held_ids), art['count']))
155
+ return None
156
+
157
+
158
+ def still_fits(chain: Sequence[Mapping[str, Any]], filt: Any, held_ids: Sequence[str]) -> Optional[str]:
159
+ """Why a chain this node holds no longer describes what it holds, or None.
160
+
161
+ Asked every time the claim goes out. A handover fits while the set this
162
+ node holds over the handed range, under the handed filter, fingerprints to
163
+ what was handed; the moment a late event lands in the closed range or
164
+ retention takes one out, the only claim it could support is a false one.
165
+ """
166
+ art = chain[-1]
167
+ if canonical(filt) != art['filter']:
168
+ return 'the filter is not the handed one'
169
+ if replication.fingerprint(held_ids) != art['fingerprint'] or len(held_ids) != art['count']:
170
+ return 'what this node holds over the handed range is no longer the handed set'
171
+ return None
172
+
173
+
174
+ def shard_config(coverage_config: Any, shard: str) -> Optional[CoverageShardConfig]:
175
+ """The operator's entry for one shard, or None if they configured none."""
176
+ for c in coverage_config or []:
177
+ if isinstance(c, dict) and c.get('shard') == shard:
178
+ return cast(CoverageShardConfig, c)
179
+ return None
@@ -0,0 +1,80 @@
1
+ """The shape of a SyncPeer as its mixins see it.
2
+
3
+ Split across core.py, initiator.py, responder.py and handover.py the way
4
+ node/reporter/ and node/peering_wire/ are: each mixin annotates `self` as
5
+ this Protocol. A declaration only; SyncPeer satisfies it structurally.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import threading
10
+ from typing import Any, Callable, Mapping, Optional, Protocol
11
+
12
+ from fleetmesh_core.wire.interfaces import NostrEvent
13
+ from fleetmesh_core.wire.wire_types import HandoverLink, SyncHandoverBody
14
+ from node.compartment import Refusals
15
+ from node.compartment import stores as st
16
+
17
+
18
+ from .types import (
19
+ SyncSessionResult,
20
+ )
21
+
22
+
23
+ class SyncShape(Protocol):
24
+ TROUBLE_WINDOW: int
25
+ refusals: Refusals
26
+ node: Any
27
+ strain: Optional[Any]
28
+ accounts: Optional[Any]
29
+ discovery: Optional[Any]
30
+ presence: Optional[Any]
31
+ session_cls: Any
32
+ ks: Any
33
+ store: Any
34
+ transport: Any
35
+ wire_client: Any
36
+ grants: Any
37
+ clock: Callable[[], int]
38
+ budget: Optional[Any]
39
+ on_session: list[Callable[[str, str, SyncSessionResult], Any]]
40
+ totals: st.Counters
41
+ aborted: st.Ring[tuple[str, str]]
42
+ refused: st.Ring[tuple[str, Optional[str], str]]
43
+ sessions: st.Table[str, tuple[str, Any, int]]
44
+ _initiated: st.Members[str]
45
+ rejected: st.Ring[tuple[str, str, str]]
46
+ unsent: st.Ring[tuple[str, str, str, int]]
47
+ completed: st.Ring[tuple[str, str, SyncSessionResult]]
48
+ handed: st.Ring[tuple[str, str, str, int]]
49
+ held: st.Ring[tuple[str, str, int]]
50
+ _offered: st.Table[tuple[str, str], str]
51
+ _lock: threading.Lock
52
+
53
+ # Compartment
54
+ def config(self, key: str, default: Any = None) -> Any: ...
55
+ def say(self, name: str, *args: Any, **kw: Any) -> Any: ...
56
+
57
+ # core.py
58
+ def _grant(self, peer_pk: str, direction: str) -> Any: ...
59
+ def _send(self, m: Mapping[str, Any], peer_pk: str) -> dict[str, tuple[bool, str]]: ...
60
+ def _seen(self, peer_pk: str, m: Mapping[str, Any]) -> None: ...
61
+ def _expire(self, now: int) -> list[str]: ...
62
+ def _finish(self, thid: str, peer_pk: str, sess: Any, status: Any) -> None: ...
63
+ def _record(self, ring: 'st.Ring[Any]', entry: tuple[Any, ...]) -> None: ...
64
+
65
+ # responder.py
66
+ def _refuse_open(self, peer_pk: str, thid: Optional[str], why: str) -> tuple[Any, ...]: ...
67
+ def _precheck(self, inbound: Any, kind: str) -> tuple[Optional[Mapping[str, Any]], str, Optional[tuple[Any, ...]]]: ...
68
+ def _send_reply(self, thid: Optional[str], peer_pk: str, sess: Any, reply: Mapping[str, Any]) -> tuple[Any, ...]: ...
69
+ def _dispatch_session(self, inbound: Any, kind: str) -> Optional[tuple[Any, ...]]: ...
70
+ def handle_open(self, inbound: Any, sender: str) -> Optional[tuple[Any, ...]]: ...
71
+ def handle_handover(self, inbound: Any, sender: str) -> Optional[tuple[Any, ...]]: ...
72
+ def handle_range(self, inbound: Any, sender: str) -> Optional[tuple[Any, ...]]: ...
73
+ def handle_need(self, inbound: Any, sender: str) -> Optional[tuple[Any, ...]]: ...
74
+ def handle_done(self, inbound: Any, sender: str) -> Optional[tuple[Any, ...]]: ...
75
+
76
+ # handover.py
77
+ def _offer_handover(self, thid: str, peer_pk: str, sess: Any, status: Any) -> Optional[str]: ...
78
+ def _unoffer(self, key: tuple[str, str], previous: Optional[str], entry: tuple[Any, ...]) -> None: ...
79
+ def _chain_for(self, shard: str, claim: NostrEvent, peer_pk: str, now: int) -> Optional[list[HandoverLink]]: ...
80
+ def _accept_handover(self, peer_pk: str, body: SyncHandoverBody) -> tuple[Any, ...]: ...
@@ -0,0 +1,51 @@
1
+ """Types and records for the Sync compartment."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Mapping, Optional, TypedDict
5
+
6
+
7
+ class SyncSessionResult(TypedDict, total=False):
8
+ """Result of a sync session (initiator or responder)."""
9
+ status: str
10
+ counters: Mapping[str, int]
11
+ findings: list[tuple[Any, ...]]
12
+ need: list[str]
13
+ dup_ratio: float
14
+ thid: str
15
+ evidence: list[Mapping[str, Any]]
16
+ grant: Optional[str]
17
+
18
+
19
+ class SyncHandedRow(TypedDict, total=False):
20
+ peer: str
21
+ shard: str
22
+ at: int
23
+
24
+
25
+ class SyncStateReport(TypedDict, total=False):
26
+ """The report SyncPeer.state() gives of itself."""
27
+ open: int
28
+ refused: int
29
+ aborted: int
30
+ completed: int
31
+ unsent: int
32
+ malformed: int
33
+ handed: list[SyncHandedRow]
34
+ held: list[SyncHandedRow]
35
+
36
+
37
+ class CoverageShardConfig(TypedDict, total=False):
38
+ """Configuration for a coverage shard."""
39
+ shard: str
40
+ filter: Mapping[str, Any]
41
+ since: int
42
+ until: int
43
+ cadence: int
44
+
45
+
46
+ class SyncTotals(TypedDict):
47
+ """Sessions since the process started, by how they ended. `state` has
48
+ always reported these as counts; the rings beside them keep the recent."""
49
+ aborted: int
50
+ refused: int
51
+ completed: int
@@ -0,0 +1,23 @@
1
+ """Transport plane: link, lan, p2p, mediation, bridge."""
2
+ from . import bridge, lan, link, mediation, p2p
3
+ from .bridge.compartment import DEFAULT_EGRESS_KINDS, DEFAULT_INGRESS_KINDS, RelayBridge
4
+ from .lan.compartment import Lan
5
+ from .link.compartment import Link, LinkState
6
+ from .mediation.compartment import Mediation
7
+ from .p2p.compartment import P2PTransport
8
+
9
+ __all__ = [
10
+ 'DEFAULT_EGRESS_KINDS',
11
+ 'DEFAULT_INGRESS_KINDS',
12
+ 'Lan',
13
+ 'Link',
14
+ 'LinkState',
15
+ 'Mediation',
16
+ 'P2PTransport',
17
+ 'RelayBridge',
18
+ 'bridge',
19
+ 'lan',
20
+ 'link',
21
+ 'mediation',
22
+ 'p2p',
23
+ ]
@@ -0,0 +1,13 @@
1
+ """Universal Nostr Relay Bridge for fleetmesh.
2
+
3
+ One-module compartment: everything is in compartment.py.
4
+
5
+ The package is `relay_bridge` and the compartment is still called `bridge`:
6
+ the name is the config key, the `meshnode bridge` verb and the health key an
7
+ operator already uses, so it stays. The package moved so that "bridge" in a
8
+ path means a compartment fronting a sidecar (node/sidecar_bridge), and this -
9
+ which fronts relays, not a sidecar process - says what it bridges.
10
+ """
11
+ from .compartment import RelayBridge
12
+
13
+ __all__ = ['RelayBridge']